diff --git a/.github/workflows/dart.yml b/.github/workflows/dart.yml
index ca11dcc..0422896 100644
--- a/.github/workflows/dart.yml
+++ b/.github/workflows/dart.yml
@@ -10,8 +10,8 @@ jobs:
build:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v4
- - uses: dart-lang/setup-dart@v1
+ - uses: actions/checkout@v7
+ - uses: dart-lang/setup-dart@v1.7.2
- name: Dart version
run: |
dart --version
@@ -34,8 +34,8 @@ jobs:
test_vm:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v4
- - uses: dart-lang/setup-dart@v1
+ - uses: actions/checkout@v7
+ - uses: dart-lang/setup-dart@v1.7.2
- name: Dart version
run: |
dart --version
@@ -51,10 +51,11 @@ jobs:
dart pub global activate coverage
dart pub global run coverage:format_coverage --packages=.dart_tool/package_config.json --report-on=lib --lcov -o ./coverage/lcov.info -i ./coverage
- name: Upload coverage to Codecov
- uses: codecov/codecov-action@v3
- env:
- CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
+ uses: codecov/codecov-action@v7
with:
+ # `codecov-action` v4+ takes the token as an input
+ # (it was an env var in v3):
+ token: ${{ secrets.CODECOV_TOKEN }}
directory: ./coverage/
flags: unittests
env_vars: OS,DART
@@ -65,8 +66,8 @@ jobs:
test_chrome:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v4
- - uses: dart-lang/setup-dart@v1
+ - uses: actions/checkout@v7
+ - uses: dart-lang/setup-dart@v1.7.2
- name: Dart version
run: |
dart --version
diff --git a/CHANGELOG.md b/CHANGELOG.md
index f81dfa5..ddf9adc 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,102 @@
+## 1.11.0
+
+- `selectByQuery` and its siblings gained 4 optional parameters, for pagination
+ and ordering:
+ - `offset`: the return offset.
+ - `page`: the 1-based page to return, an ergonomic alternative to `offset`
+ that computes it from the page size: `(page - 1) * limit`.
+ - `orderByID`: orders the result by the table's ID column, resolved
+ automatically from the existing scheme machinery (`TableScheme.idFieldName`
+ → `EncodingContext.tableFieldID`, or `EntityHandler.idFieldName`).
+ - `orderDirection`: the new `OrderDirection` enum, `ascending` (default) or
+ `descending`.
+
+ Semantics:
+ - The effective ordering is `orderByID ?? (offset != null)`: a non-null
+ `offset` turns the ordering **on** by default, since an offset-based
+ pagination needs a stable order to be correct. Pass `orderByID: false` to
+ opt out and get a bare `OFFSET`.
+ - `orderDirection` is **ignored** while the ordering is not active.
+ - `page` is a public convenience resolved to an `offset` at the repository
+ layer (see `resolveSelectOffset`); the adapter contract keeps taking only
+ `offset`. It throws an `ArgumentError` when combined with an `offset` (two
+ spellings of one thing), when there is no positive `limit` to use as the
+ page size, or when it is `< 1`. `page: 1` resolves to `offset: 0`, which
+ still activates the ordering, so even the first page is stable.
+ - All 4 are optional and default to the previous behavior: with them unset the
+ generated SQL is character-identical to 1.10.0.
+
+ Added to `EntitySource`/`EntityRepository` (`selectByQuery`,
+ `selectFirstByQuery`, `select`, `selectIDsByQuery`, `selectIDsBy`,
+ `selectAll`), `APIRepository`, `IterableEntityRepository`
+ (`matches`/`all` included), `DBEntityRepository`, `DBRelationalAdapter`/
+ `DBRelationalRepositoryAdapter`/`DBRelationalEntityRepository`,
+ `DBAdapter.doSelectAll`/`doSelectByIDs`, `DBSQLAdapter.doSelect`/
+ `doSelectIDsBy`/`generateSelectSQL`/`generateSelectIDsSQL` and
+ `DBSQLRepositoryAdapter.generateSelectSQL`.
+
+- New `OrderDirection` enum (`bones_api_types.dart`), with `sqlKeyword`,
+ `parse` and the resolvers `resolve` and `resolveOrderByID` that state the
+ semantics above exactly once.
+
+- New `compareEntityIDs` and `applySelectOrderAndPagination`
+ (`bones_api_entity.dart`): the shared Dart-side "order by ID → skip → take"
+ used by every adapter that can't delegate the ordering to a DB engine.
+
+- New `resolveSelectOffset` (`bones_api_entity.dart`): resolves `page` to an
+ `offset`, and states the `page`/`offset`/`limit` validation rules once.
+
+- `SQLDialect`:
+ - New `orderBySQL` and `limitOffsetSQL` clause builders, so all the
+ dialect-specific `SELECT` tail syntax lives in one place.
+ - New `offsetRequiresLimit` and `offsetMaxLimitValue` capabilities. MySQL sets
+ `offsetRequiresLimit: true` since it can't parse an `OFFSET` that is not
+ preceded by a `LIMIT`; an offset-only select there emits
+ `LIMIT 18446744073709551615 OFFSET n`. PostgreSQL and the `generic`
+ (in-memory) dialect emit a bare `OFFSET n`.
+
+- `SQL`: new `offset`, `orderByID` and `orderDirection` fields (carried by
+ `copy()`), read by `DBSQLMemoryAdapter` to apply the same semantics in Dart.
+
+- `APIDBModule.select` (`/db/select/
`): new `LIMIT=`, `OFFSET=`,
+ `PAGE=` and `ORDER=asc|desc` query directives
+ (see `APIDBModule.selectQueryDirectives`),
+ parsed from the query `String` alongside the pre-existing `EAGER=true` and
+ stripped before the remainder is parsed as the entity condition query. The
+ endpoint no longer selects the whole table and sorts it in Dart — the ordering
+ is now resolved by the DB. Its output order is unchanged. An invalid `PAGE`
+ becomes an error response rather than an uncaught `ArgumentError`.
+
+- **Behavior change**: `limit` is now honored on the paths that previously
+ accepted and silently ignored it — `DBEntityRepository.select`'s
+ `ConditionID`/`ConditionIdIN`/`ConditionANY`/`KeyConditionEQ` fast paths,
+ `DBAdapter.doSelectAll`/`doSelectByIDs`, and the `DBObjectMemoryAdapter`,
+ `DBObjectDirectoryAdapter` and `DBObjectGCSAdapter` adapters. For example,
+ `selectAll(limit: 2)` on an object adapter returned *every* row before this
+ release; it now returns 2.
+
+- **Source-breaking for external subclasses**: new named parameters were added
+ to abstract members (`EntitySource.select`/`selectIDsBy`/`selectAll`,
+ `DBAdapter.doSelectAll`/`doSelectByIDs`,
+ `DBRelationalAdapter.doSelect`/`doSelectIDsBy`). Dart requires an override to
+ accept every named parameter of the supertype, so third-party
+ `EntityRepository`/`DBAdapter` implementations must widen their overrides.
+
+- Known limitation: a query over a to-many relationship generates a `JOIN`
+ without a `DISTINCT`, so it can return the same entity more than once
+ (pre-existing). Paginating such a query is therefore best-effort.
+
+- Tests:
+ - New `bones_api_entity_select_order_test.dart` (`OrderDirection`,
+ `compareEntityIDs`, `applySelectOrderAndPagination`, `SQLDialect` clause
+ builders), `bones_api_entity_db_sql_select_test.dart` (exact generated SQL
+ per case + end-to-end paging over the in-memory SQL adapter) and
+ `bones_api_db_module_test.dart` (first coverage of `APIDBModule`).
+ - `bones_api_entity_db_tests_base.dart`: 3 new tests in the shared adapter
+ template, so the generated SQL and real page-by-page reads are asserted for
+ the in-memory, PostgreSQL, MySQL, object-memory and object-directory
+ adapters. Verified against real PostgreSQL and MySQL containers.
+
## 1.10.0
- `docker_commander`: `^2.1.8` → `^3.0.0`.
diff --git a/README.md b/README.md
index 11330b6..c960e79 100644
--- a/README.md
+++ b/README.md
@@ -416,10 +416,49 @@ class AccountAPIRepository extends APIRepository {
// This condition will be translated to a SQL with INNER JOIN (when using an SQLAdapter):
return selectByQuery(' address.state == ? ', parameters: [state]);
}
+
+ /// Selects a page of [Account]s by field `state` (`page` starts at 1):
+ FutureOr> selectAccountsPage(String state, int page) {
+ // `page` computes the offset from the page size, and implies `orderByID`,
+ // so the pagination is stable
+ // (translated to: ORDER BY ASC LIMIT 20 OFFSET <(page - 1) * 20>).
+ return selectByQuery(
+ ' address.state == ? ',
+ parameters: [state],
+ limit: 20,
+ page: page,
+ );
+ }
+
+ /// Selects the 10 newest [Account]s (highest IDs first):
+ FutureOr> selectNewestAccounts() {
+ return selectAll(
+ limit: 10,
+ orderByID: true,
+ orderDirection: OrderDirection.descending,
+ );
+ }
}
```
+### Ordering and pagination
+
+The `select*` methods accept `limit`, `offset`, `page`, `orderByID` and
+`orderDirection`:
+
+- `orderByID` orders by the table's ID column, resolved automatically from the
+ table scheme — no column name to spell out.
+- A non-null `offset` turns `orderByID` on by default, since an offset without
+ a stable order can return overlapping or missing rows across pages. Pass
+ `orderByID: false` to opt out.
+- `orderDirection` is `OrderDirection.ascending` by default, and is ignored
+ while the ordering is not active.
+- `page` is the 1-based ergonomic form of `offset`, using `limit` as the page
+ size: `page: 3, limit: 20` is `offset: 40`. It throws an `ArgumentError` if
+ combined with an `offset`, if there is no positive `limit` to page by, or if
+ it is below 1.
+
The config file used above:
File: `api-local.yaml`
diff --git a/lib/src/bones_api_base.dart b/lib/src/bones_api_base.dart
index 9456c2b..9953b6c 100644
--- a/lib/src/bones_api_base.dart
+++ b/lib/src/bones_api_base.dart
@@ -48,7 +48,7 @@ typedef APILogger =
/// Bones API Library class.
class BonesAPI {
// ignore: constant_identifier_names
- static const String VERSION = '1.10.0';
+ static const String VERSION = '1.11.0';
static bool _boot = false;
diff --git a/lib/src/bones_api_db_module.dart b/lib/src/bones_api_db_module.dart
index b091f85..758b59e 100644
--- a/lib/src/bones_api_db_module.dart
+++ b/lib/src/bones_api_db_module.dart
@@ -15,6 +15,7 @@ import 'bones_api_entity_rules.dart';
import 'bones_api_extension.dart';
import 'bones_api_html_document.dart';
import 'bones_api_module.dart';
+import 'bones_api_types.dart';
import 'bones_api_utils_json.dart';
final _log = logging.Logger('APIDBModule');
@@ -263,11 +264,68 @@ class APIDBModule extends APIModule {
return APIResponse.ok(html, mimeType: 'html');
}
+ /// The directives accepted by the query `String` of [select], as
+ /// `&`-joined `KEY=VALUE` tokens. Everything else in the query `String` is
+ /// the entity condition query.
+ ///
+ /// - `EAGER=true`: resolves the referenced entities.
+ /// - `LIMIT=`: the maximum number of returned entities, and the page size
+ /// of `PAGE`.
+ /// - `OFFSET=`: the return offset, for pagination.
+ /// - `PAGE=`: the 1-based page to return; requires `LIMIT` and can't be
+ /// combined with `OFFSET`. See [resolveSelectOffset].
+ /// - `ORDER=asc|desc`: the [OrderDirection] of the ordering.
+ ///
+ /// Example: `/db/select/user/json?email == "a@b.c"&LIMIT=10&PAGE=3`
+ static const List selectQueryDirectives = [
+ 'EAGER',
+ 'LIMIT',
+ 'OFFSET',
+ 'PAGE',
+ 'ORDER',
+ ];
+
+ /// Extracts the `KEY=VALUE` directive [key] from the raw [query] `String`,
+ /// returning the value and the [query] without it.
+ ///
+ /// Follows the pre-existing `EAGER=true` convention: the directive can be
+ /// the whole [query] or one of its `&`-joined tokens.
+ static ({String query, String? value}) _extractQueryDirective(
+ String query,
+ String key,
+ ) {
+ if (query.isEmpty) return (query: query, value: null);
+
+ var prefix = '$key=';
+
+ var parts = query.split('&');
+
+ String? value;
+ var rest = [];
+
+ for (var part in parts) {
+ if (value == null && part.startsWith(prefix)) {
+ value = part.substring(prefix.length);
+ } else {
+ rest.add(part);
+ }
+ }
+
+ if (value == null) return (query: query, value: null);
+
+ return (query: rest.join('&'), value: value);
+ }
+
Future> select(
String table,
APIRequest apiRequest, {
bool? eager,
bool json = false,
+ int? limit,
+ int? offset,
+ int? page,
+ bool? orderByID,
+ OrderDirection? orderDirection,
}) async {
if (onlyOnDevelopment && !development) {
return APIResponse.error(error: "Unsupported request!");
@@ -287,20 +345,62 @@ class APIDBModule extends APIModule {
var query = Uri.decodeQueryComponent(requestedUri.query);
- if (query == 'EAGER=true') {
- query = '';
- eager = true;
- } else if (query.endsWith('&EAGER=true')) {
- query = query.substring(0, query.length - 11);
- eager = true;
+ // See [selectQueryDirectives]. The arguments passed to this method take
+ // precedence over the query `String` directives.
+ {
+ var extracted = _extractQueryDirective(query, 'EAGER');
+ query = extracted.query;
+ if (extracted.value == 'true') eager = true;
+ }
+
+ {
+ var extracted = _extractQueryDirective(query, 'LIMIT');
+ query = extracted.query;
+ limit ??= int.tryParse(extracted.value?.trim() ?? '');
+ }
+
+ {
+ var extracted = _extractQueryDirective(query, 'OFFSET');
+ query = extracted.query;
+ offset ??= int.tryParse(extracted.value?.trim() ?? '');
+ }
+
+ {
+ var extracted = _extractQueryDirective(query, 'PAGE');
+ query = extracted.query;
+ page ??= int.tryParse(extracted.value?.trim() ?? '');
+ }
+
+ {
+ var extracted = _extractQueryDirective(query, 'ORDER');
+ query = extracted.query;
+ orderDirection ??= OrderDirection.parse(extracted.value);
+ }
+
+ // Resolved here (and not by the repository) so an invalid `PAGE` becomes an
+ // error response instead of an uncaught `ArgumentError`:
+ try {
+ offset = resolveSelectOffset(page: page, offset: offset, limit: limit);
+ } on ArgumentError catch (e) {
+ return APIResponse.error(error: "Invalid pagination: ${e.message}");
}
eager ??= false;
+ // This endpoint has always returned ID-sorted entities, so the ordering
+ // defaults to `true` here (and not to the `offset != null` rule).
+ // It is now resolved by the DB instead of being sorted in Dart.
+ orderByID ??= true;
+
_log.info(
"APIDBModule[REQUEST]> select> "
"table: `$table` ; "
- "eager: $eager"
+ "eager: $eager ; "
+ "orderByID: $orderByID ; "
+ "orderDirection: ${OrderDirection.resolve(orderDirection).name}"
+ "${limit != null ? ' ; limit: $limit' : ''}"
+ "${offset != null ? ' ; offset: $offset' : ''}"
+ "${page != null ? ' ; page: $page' : ''}"
"${query.isNotEmpty ? ' ; QUERY> $query' : ''}",
);
@@ -311,35 +411,24 @@ class APIDBModule extends APIModule {
if (query.isEmpty) {
var selectAll = await entityRepository.selectAll(
resolutionRules: resolutionRules,
+ limit: limit,
+ offset: offset,
+ orderByID: orderByID,
+ orderDirection: orderDirection,
);
list = selectAll.toList();
} else {
var selectByQuery = await entityRepository.selectByQuery(
query,
resolutionRules: resolutionRules,
+ limit: limit,
+ offset: offset,
+ orderByID: orderByID,
+ orderDirection: orderDirection,
);
list = selectByQuery.toList();
}
- list.sort((a, b) {
- var id1 = entityRepository.getEntityID(a);
- var id2 = entityRepository.getEntityID(b);
-
- if (id1 == null && id2 == null) {
- return 0;
- } else if (id1 == null) {
- return 1;
- } else if (id2 == null) {
- return -1;
- } else if (id1 is num && id2 is num) {
- return id1.compareTo(id2);
- } else if (id1 is String && id2 is String) {
- return id1.compareTo(id2);
- } else {
- return 0;
- }
- });
-
if (json) {
var entitiesJson = _entitiesToJsonMap(list);
return APIResponse.ok(entitiesJson, mimeType: 'json');
diff --git a/lib/src/bones_api_entity.dart b/lib/src/bones_api_entity.dart
index 2799a8d..72a9d6c 100644
--- a/lib/src/bones_api_entity.dart
+++ b/lib/src/bones_api_entity.dart
@@ -3307,6 +3307,117 @@ mixin EntityFieldAccessor {
class EntityFieldAccessorGeneric with EntityFieldAccessor {}
+/// Compares 2 entity IDs, implementing the `orderByID` ordering of the
+/// `select*` methods.
+///
+/// - `null` IDs are ordered last (in [OrderDirection.ascending] order).
+/// - [num]s are compared numerically and [String]s lexicographically.
+/// - Any other pair of [Comparable]s of the same type is compared
+/// with [Comparable.compareTo]; otherwise returns `0` (undefined order).
+int compareEntityIDs(Object? id1, Object? id2) {
+ if (identical(id1, id2)) return 0;
+
+ if (id1 == null) return id2 == null ? 0 : 1;
+ if (id2 == null) return -1;
+
+ if (id1 is num && id2 is num) return id1.compareTo(id2);
+ if (id1 is String && id2 is String) return id1.compareTo(id2);
+
+ if (id1 is Comparable && id2 is Comparable) {
+ if (id1.runtimeType == id2.runtimeType) {
+ return id1.compareTo(id2);
+ }
+ }
+
+ return 0;
+}
+
+/// Resolves the effective `offset` of a `select*` operation from a 1-based
+/// [page], where [limit] is the page size.
+///
+/// Returns [offset] unchanged when [page] is `null`, otherwise
+/// `(page - 1) * limit`. A [page] of `1` resolves to an `offset` of `0`, which
+/// still activates the ordering (see [OrderDirection.resolveOrderByID]), so a
+/// paginated select is stable by default.
+///
+/// Throws an [ArgumentError] when:
+/// - [page] and [offset] are both defined: they are two spellings of the same
+/// thing, so passing both is a bug rather than a precedence question;
+/// - [page] is defined without a positive [limit]: a page has no meaning
+/// without a page size, and a `limit` of `0` means "no limit";
+/// - [page] is `< 1`: pages are numbered from `1`.
+int? resolveSelectOffset({int? page, int? offset, int? limit}) {
+ if (page == null) return offset;
+
+ if (offset != null) {
+ throw ArgumentError.value(
+ page,
+ 'page',
+ "`page` and `offset` are mutually exclusive (offset: $offset)",
+ );
+ }
+
+ if (limit == null || limit <= 0) {
+ throw ArgumentError.value(
+ page,
+ 'page',
+ '`page` requires a positive `limit` (the page size), got: $limit',
+ );
+ }
+
+ if (page < 1) {
+ throw ArgumentError.value(page, 'page', '`page` is 1-based, must be >= 1');
+ }
+
+ return (page - 1) * limit;
+}
+
+/// Applies the ordering and the pagination of a `select*` operation to [itr].
+///
+/// Orders the elements by ID (resolved through [idGetter]) when
+/// [OrderDirection.resolveOrderByID] resolves to `true`, then skips [offset]
+/// elements and takes [limit] of them — in that order, matching SQL semantics
+/// (`ORDER BY` → `OFFSET` → `LIMIT`).
+///
+/// Used by the [DBAdapter]s that can't delegate the ordering and the
+/// pagination to a DB engine, and by [IterableEntityRepository].
+///
+/// [zeroLimitIsUnlimited] selects how a `limit` of `0` is interpreted, since
+/// the pre-existing call sites disagree: the generated SQL treats it as "no
+/// `LIMIT` clause" (`true`), while the in-memory selects treat it as an empty
+/// result (`false`, the default). Only relevant for a [limit] of exactly `0`.
+Iterable applySelectOrderAndPagination(
+ Iterable itr,
+ Object? Function(T o) idGetter, {
+ int? limit,
+ int? offset,
+ bool? orderByID,
+ OrderDirection? orderDirection,
+ bool zeroLimitIsUnlimited = false,
+}) {
+ if (OrderDirection.resolveOrderByID(orderByID, offset)) {
+ var descending = OrderDirection.resolve(orderDirection).isDescending;
+
+ var sorted = itr.toList();
+ sorted.sort(
+ descending
+ ? (a, b) => compareEntityIDs(idGetter(b), idGetter(a))
+ : (a, b) => compareEntityIDs(idGetter(a), idGetter(b)),
+ );
+ itr = sorted;
+ }
+
+ if (offset != null && offset > 0) {
+ itr = itr.skip(offset);
+ }
+
+ if (limit != null && (zeroLimitIsUnlimited ? limit > 0 : limit >= 0)) {
+ itr = itr.take(limit);
+ }
+
+ return itr;
+}
+
abstract class EntityAccessor {
static String simplifiedName(String name) {
name = name.trim().toLowerCase().replaceAll(RegExp(r'[\W_]+'), '').trim();
@@ -3381,20 +3492,51 @@ abstract class EntitySource extends EntityAccessor {
final ConditionParseCache _parseCache = ConditionParseCache.get();
+ /// {@template bones_api.select_pagination}
+ /// Ordering and pagination:
+ /// - [limit]: the maximum number of returned entities. Also the page size
+ /// of [page].
+ /// - [offset]: the return offset, for pagination.
+ /// - [page]: the 1-based page to return, an ergonomic alternative to
+ /// [offset]: it resolves to `(page - 1) * limit`. Requires a positive
+ /// [limit], and can't be combined with [offset].
+ /// See [resolveSelectOffset].
+ /// - [orderByID]: if `true` the result is ordered by the table ID column,
+ /// automatically resolved from the table scheme
+ /// ([TableScheme.idFieldName]) or from [EntityHandler.idFieldName].
+ /// Defaults to `true` when [offset] is defined, since a paginated select
+ /// needs a stable order to be correct. Pass `false` to opt out.
+ /// - [orderDirection]: the [OrderDirection] of the ordering,
+ /// [OrderDirection.ascending] by default.
+ /// **Ignored while the ordering is not active** (see [orderByID]).
+ ///
+ /// Note that a query over a to-many relationship generates a `JOIN` without
+ /// a `DISTINCT`, so it can return the same entity more than once. Paginating
+ /// such a query is best-effort.
+ /// {@endtemplate}
FutureOr selectFirstByQuery(
String query, {
Object? parameters,
List? positionalParameters,
Map? namedParameters,
Transaction? transaction,
+ int? offset,
+ int? page,
+ bool? orderByID,
+ OrderDirection? orderDirection,
}) => selectByQuery(
query,
parameters: parameters,
namedParameters: namedParameters,
transaction: transaction,
limit: 1,
+ // The page size of a "first" select is 1, so `page: n` is the Nth entity:
+ offset: resolveSelectOffset(page: page, offset: offset, limit: 1),
+ orderByID: orderByID,
+ orderDirection: orderDirection,
).resolveMapped((result) => result.firstOrNull);
+ /// {@macro bones_api.select_pagination}
FutureOr> selectByQuery(
String query, {
Object? parameters,
@@ -3402,6 +3544,10 @@ abstract class EntitySource extends EntityAccessor {
Map? namedParameters,
Transaction? transaction,
int? limit,
+ int? offset,
+ int? page,
+ bool? orderByID,
+ OrderDirection? orderDirection,
}) {
var condition = _parseCache.parseQuery(query);
@@ -3412,9 +3558,13 @@ abstract class EntitySource extends EntityAccessor {
namedParameters: namedParameters,
transaction: transaction,
limit: limit,
+ offset: resolveSelectOffset(page: page, offset: offset, limit: limit),
+ orderByID: orderByID,
+ orderDirection: orderDirection,
);
}
+ /// {@macro bones_api.select_pagination}
FutureOr> select(
EntityMatcher matcher, {
Object? parameters,
@@ -3422,9 +3572,14 @@ abstract class EntitySource extends EntityAccessor {
Map? namedParameters,
Transaction? transaction,
int? limit,
+ int? offset,
+ int? page,
+ bool? orderByID,
+ OrderDirection? orderDirection,
EntityResolutionRules? resolutionRules,
});
+ /// {@macro bones_api.select_pagination}
FutureOr> selectIDsByQuery(
String query, {
Object? parameters,
@@ -3432,6 +3587,10 @@ abstract class EntitySource extends EntityAccessor {
Map? namedParameters,
Transaction? transaction,
int? limit,
+ int? offset,
+ int? page,
+ bool? orderByID,
+ OrderDirection? orderDirection,
}) {
var condition = _parseCache.parseQuery(query);
@@ -3442,9 +3601,13 @@ abstract class EntitySource extends EntityAccessor {
namedParameters: namedParameters,
transaction: transaction,
limit: limit,
+ offset: resolveSelectOffset(page: page, offset: offset, limit: limit),
+ orderByID: orderByID,
+ orderDirection: orderDirection,
);
}
+ /// {@macro bones_api.select_pagination}
FutureOr> selectIDsBy(
EntityMatcher matcher, {
Object? parameters,
@@ -3452,11 +3615,20 @@ abstract class EntitySource extends EntityAccessor {
Map? namedParameters,
Transaction? transaction,
int? limit,
+ int? offset,
+ int? page,
+ bool? orderByID,
+ OrderDirection? orderDirection,
});
+ /// {@macro bones_api.select_pagination}
FutureOr> selectAll({
Transaction? transaction,
int? limit,
+ int? offset,
+ int? page,
+ bool? orderByID,
+ OrderDirection? orderDirection,
EntityResolutionRules? resolutionRules,
});
@@ -5463,6 +5635,7 @@ abstract class EntityRepository extends EntityAccessor
);
}
+ /// {@macro bones_api.select_pagination}
@override
FutureOr selectFirstByQuery(
String query, {
@@ -5470,6 +5643,10 @@ abstract class EntityRepository extends EntityAccessor
List? positionalParameters,
Map? namedParameters,
Transaction? transaction,
+ int? offset,
+ int? page,
+ bool? orderByID,
+ OrderDirection? orderDirection,
EntityResolutionRules? resolutionRules,
}) => selectByQuery(
query,
@@ -5477,9 +5654,14 @@ abstract class EntityRepository extends EntityAccessor
namedParameters: namedParameters,
transaction: transaction,
limit: 1,
+ // The page size of a "first" select is 1, so `page: n` is the Nth entity:
+ offset: resolveSelectOffset(page: page, offset: offset, limit: 1),
+ orderByID: orderByID,
+ orderDirection: orderDirection,
resolutionRules: resolutionRules,
).resolveMapped((result) => result.firstOrNull);
+ /// {@macro bones_api.select_pagination}
@override
FutureOr> selectByQuery(
String query, {
@@ -5488,6 +5670,10 @@ abstract class EntityRepository extends EntityAccessor
Map? namedParameters,
Transaction? transaction,
int? limit,
+ int? offset,
+ int? page,
+ bool? orderByID,
+ OrderDirection? orderDirection,
EntityResolutionRules? resolutionRules,
}) {
checkNotClosed();
@@ -5501,10 +5687,14 @@ abstract class EntityRepository extends EntityAccessor
namedParameters: namedParameters,
transaction: transaction,
limit: limit,
+ offset: resolveSelectOffset(page: page, offset: offset, limit: limit),
+ orderByID: orderByID,
+ orderDirection: orderDirection,
resolutionRules: resolutionRules,
);
}
+ /// {@macro bones_api.select_pagination}
@override
FutureOr> selectIDsByQuery(
String query, {
@@ -5513,6 +5703,10 @@ abstract class EntityRepository extends EntityAccessor
Map? namedParameters,
Transaction? transaction,
int? limit,
+ int? offset,
+ int? page,
+ bool? orderByID,
+ OrderDirection? orderDirection,
}) {
checkNotClosed();
@@ -5525,6 +5719,9 @@ abstract class EntityRepository extends EntityAccessor
namedParameters: namedParameters,
transaction: transaction,
limit: limit,
+ offset: resolveSelectOffset(page: page, offset: offset, limit: limit),
+ orderByID: orderByID,
+ orderDirection: orderDirection,
);
}
@@ -7427,6 +7624,10 @@ abstract class IterableEntityRepository
Map? namedParameters,
Transaction? transaction,
int? limit,
+ int? offset,
+ int? page,
+ bool? orderByID,
+ OrderDirection? orderDirection,
EntityResolutionRules? resolutionRules,
}) {
checkNotClosed();
@@ -7437,6 +7638,9 @@ abstract class IterableEntityRepository
positionalParameters: positionalParameters,
namedParameters: namedParameters,
limit: limit,
+ offset: resolveSelectOffset(page: page, offset: offset, limit: limit),
+ orderByID: orderByID,
+ orderDirection: orderDirection,
);
return trackEntities(os);
@@ -7450,6 +7654,10 @@ abstract class IterableEntityRepository
Map? namedParameters,
Transaction? transaction,
int? limit,
+ int? offset,
+ int? page,
+ bool? orderByID,
+ OrderDirection? orderDirection,
}) {
checkNotClosed();
@@ -7459,6 +7667,9 @@ abstract class IterableEntityRepository
positionalParameters: positionalParameters,
namedParameters: namedParameters,
limit: limit,
+ offset: resolveSelectOffset(page: page, offset: offset, limit: limit),
+ orderByID: orderByID,
+ orderDirection: orderDirection,
);
var ids = entityHandler.getIDs(os);
@@ -7470,11 +7681,20 @@ abstract class IterableEntityRepository
FutureOr> selectAll({
Transaction? transaction,
int? limit,
+ int? offset,
+ int? page,
+ bool? orderByID,
+ OrderDirection? orderDirection,
EntityResolutionRules? resolutionRules,
}) {
checkNotClosed();
- var os = all(limit: limit);
+ var os = all(
+ limit: limit,
+ offset: resolveSelectOffset(page: page, offset: offset, limit: limit),
+ orderByID: orderByID,
+ orderDirection: orderDirection,
+ );
return trackEntities(os);
}
@@ -7871,12 +8091,16 @@ abstract class IterableEntityRepository
return del;
}
+ /// {@macro bones_api.select_pagination}
List matches(
EntityMatcher matcher, {
Object? parameters,
List? positionalParameters,
Map? namedParameters,
int? limit,
+ int? offset,
+ bool? orderByID,
+ OrderDirection? orderDirection,
}) {
var itr = iterable().where((o) {
return matcher.matchesEntity(
@@ -7888,22 +8112,47 @@ abstract class IterableEntityRepository
);
});
- if (limit != null && limit > 0) {
- itr = itr.take(limit);
- }
-
- return itr.toList();
+ return _applyOrderAndPagination(
+ itr,
+ limit: limit,
+ offset: offset,
+ orderByID: orderByID,
+ orderDirection: orderDirection,
+ );
}
- List all({int? limit}) {
- var itr = iterable();
-
- if (limit != null && limit > 0) {
- itr = itr.take(limit);
- }
+ /// {@macro bones_api.select_pagination}
+ List all({
+ int? limit,
+ int? offset,
+ bool? orderByID,
+ OrderDirection? orderDirection,
+ }) => _applyOrderAndPagination(
+ iterable(),
+ limit: limit,
+ offset: offset,
+ orderByID: orderByID,
+ orderDirection: orderDirection,
+ );
- return itr.toList();
- }
+ List _applyOrderAndPagination(
+ Iterable itr, {
+ int? limit,
+ int? offset,
+ bool? orderByID,
+ OrderDirection? orderDirection,
+ }) =>
+ applySelectOrderAndPagination(
+ itr,
+ (o) => getID(o, entityHandler: entityHandler),
+ limit: limit,
+ offset: offset,
+ orderByID: orderByID,
+ orderDirection: orderDirection,
+ // Preserves the pre-existing behavior of this repository,
+ // where a `limit` of 0 means "no limit":
+ zeroLimitIsUnlimited: true,
+ ).toList();
@override
Map information({bool extended = false}) {
diff --git a/lib/src/bones_api_entity_db.dart b/lib/src/bones_api_entity_db.dart
index 3a06214..0b7beaa 100644
--- a/lib/src/bones_api_entity_db.dart
+++ b/lib/src/bones_api_entity_db.dart
@@ -603,18 +603,28 @@ abstract class DBAdapter extends SchemeProvider
PreFinishDBOperation