Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 33 additions & 3 deletions MIGRATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -2651,14 +2652,43 @@ final authors = await supabase
Authors.name,
Authors.books.select([Books.title]),
])
.limit(3, referencedTable: 'books');
.embedded(Authors.books, (books) => books.limit(3));
final List<String> titles = [
for (final book in authors.first.read(Authors.books))
book.read(Books.title),
];
```

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.

Expand Down
1 change: 0 additions & 1 deletion packages/postgrest/lib/postgrest.dart
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ export 'src/postgrest.dart';
export 'src/postgrest_builder.dart';
export 'src/postgrest_typed_builder.dart'
hide
PostgrestEmbeddedPage,
PostgrestExplainOptions,
PostgrestHttpTableExecutor,
PostgrestRequestOptions,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,8 @@ final class PostgrestToOneRelation<Row, Target>
/// 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<Row, Target>
extends PostgrestRelation<Row, Target>
Expand Down
101 changes: 101 additions & 0 deletions packages/postgrest/lib/src/postgrest_embedded_scope.dart
Original file line number Diff line number Diff line change
@@ -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<Row> {
PostgrestEmbeddedScope._(
this._relation, {
List<PostgrestOrdering<Row>> orderings = const [],
int? limit,
int? offset,
List<PostgrestEmbeddedScope<Object?>> scopes = const [],
}) : _orderings = List.unmodifiable(orderings),
_limit = limit,
_offset = offset,
_scopes = List.unmodifiable(scopes);

final PostgrestToManyRelation<Object?, Row> _relation;
final List<PostgrestOrdering<Row>> _orderings;
final int? _limit;
final int? _offset;
final List<PostgrestEmbeddedScope<Object?>> _scopes;

PostgrestEmbeddedScope<Row> _copyWith({
List<PostgrestOrdering<Row>>? orderings,
int? limit,
int? offset,
List<PostgrestEmbeddedScope<Object?>>? 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<Row> order(PostgrestOrdering<Row> ordering) =>
_copyWith(orderings: [..._orderings, ordering]);

/// Limits the embedded rows of each parent row to [count].
PostgrestEmbeddedScope<Row> limit(int count) => _copyWith(limit: count);

/// Limits the embedded rows of each parent row to the ones within the
/// specified range, inclusive.
PostgrestEmbeddedScope<Row> 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<Row> embedded<Target>(
PostgrestToManyRelation<Row, Target> relation,
PostgrestEmbeddedScope<Target> Function(
PostgrestEmbeddedScope<Target> 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<String> orderKeys,
int? limit,
int? offset,
});
45 changes: 19 additions & 26 deletions packages/postgrest/lib/src/postgrest_table_executor.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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<T> _applyFilter<T>(
PostgrestFilterBuilder<T> builder,
PostgrestTableRequest request,
Expand All @@ -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;
}

Expand All @@ -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();
Expand Down
29 changes: 7 additions & 22 deletions packages/postgrest/lib/src/postgrest_table_request.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand All @@ -180,7 +165,7 @@ final class PostgrestTableRequest {
List<PostgrestOrdering<Object?>> orderings = const [],
this.limit,
this.offset,
List<PostgrestEmbeddedPage> embeddedPages = const [],
List<PostgrestEmbeddedScope<Object?>> embeddedScopes = const [],
this.shape = PostgrestResultShape.rows,
this.countOption,
this.payload,
Expand All @@ -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.
Expand Down Expand Up @@ -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<PostgrestEmbeddedPage> embeddedPages;
/// The sort keys and row windows applied inside to-many embeds.
final List<PostgrestEmbeddedScope<Object?>> embeddedScopes;

/// The form the result is asked back in.
final PostgrestResultShape shape;
Expand Down Expand Up @@ -291,7 +276,7 @@ final class PostgrestTableRequest {
List<PostgrestOrdering<Object?>>? orderings,
int? limit,
int? offset,
List<PostgrestEmbeddedPage>? embeddedPages,
List<PostgrestEmbeddedScope<Object?>>? embeddedScopes,
PostgrestResultShape? shape,
CountOption? countOption,
Object? payload,
Expand All @@ -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,
Expand Down
17 changes: 12 additions & 5 deletions packages/postgrest/lib/src/postgrest_transform_builder.dart
Original file line number Diff line number Diff line change
Expand Up @@ -119,14 +119,21 @@ class PostgrestTransformBuilder<T> extends PostgrestBuilder<T> {
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<T> appendOrderKey(String key) {
final existingOrder = _url.queryParameters['order'];
PostgrestTransformBuilder<T> 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)),
);
}

Expand Down
1 change: 1 addition & 0 deletions packages/postgrest/lib/src/postgrest_typed_builder.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
Loading
Loading