- name
- sembast-query
- description
- Use when querying or listening to package:sembast data: Finder, Filter (equals, greaterThan, inList, matches, arrayContains, and/or/not, custom), SortOrder, Boundary paging, limit/offset, nested and list field paths (a.b, tag.0, items.@), Field.key, store.find/findFirst/findKeys/count, store.query() and QueryRef.getSnapshots/getSnapshot/count/delete, reactive streams record.onSnapshot, query.onSnapshots, onCount, and the synchronous getSync/findSync variants. Writes and transactions are in sembast-crud.
# sembast: queries, filters, sorting and listeners
Sembast keeps the whole database in memory, so queries are in-memory
filtering and sorting of a store. A `Finder` describes the query; run it once
with `store.find` / `store.findFirst` / `store.query(finder: f)`, or subscribe
to results with `onSnapshot(s)` streams that re-emit after each commit.
```dart
import 'package:sembast/sembast.dart';
final animals = intMapStoreFactory.store('animals');
Future<void> demo(Database db) async {
var finder = Finder(
filter: Filter.greaterThan('age', 2) & Filter.equals('kind', 'cat'),
sortOrders: [SortOrder('name')],
limit: 20,
);
var snapshots = await animals.find(db, finder: finder);
for (var snapshot in snapshots) {
print('${snapshot.key}: ${snapshot['name']}');
}
}
```
## Guidelines
### Reading by key
* Fastest path: `store.record(key).get(client)` (value or null),
`getSnapshot(client)`, `exists(client)`; several keys with
`store.records(keys).get(client)` (a list with `null` for missing keys).
* A `RecordSnapshot` has `ref`, `key`, `value` and `operator [](field)` which
resolves dotted paths (`snapshot['address.city']`), `Field.key` and
`Field.value`. On an `Iterable<RecordSnapshot>` use `.keys`, `.values` and
`.keysAndValues`.
* Snapshot values are read-only; clone with `cloneMap` from
`package:sembast/utils/value_utils.dart` before editing.
### Finder
* `Finder({filter, sortOrders, limit, offset, start, end})`. Everything is
optional; an empty `Finder()` matches every record. Order of application:
filter, sort, boundaries, offset, limit.
* `store.find(client, finder: f)` returns `List<RecordSnapshot>`;
`findFirst` the first or null; `findKeys` / `findKey` only keys;
`store.count(client, filter: filter)` takes a `Filter`, not a `Finder`.
* `store.query(finder: f)` returns a reusable `QueryRef` with
`getSnapshots`, `getSnapshot`, `getKeys`, `getKey`, `count`, `delete` (all
taking a `DatabaseClient`) and the streams below (taking a `Database`).
* `store.stream(client, filter: filter)` iterates records unsorted without
building a list; use it for one pass over a large store.
### Filters
* Field comparison: `Filter.equals(field, value)`, `notEquals`, `isNull`,
`notNull`, `lessThan`, `lessThanOrEquals`, `greaterThan`,
`greaterThanOrEquals`, `inList(field, list)`.
* Text: `Filter.matches(field, pattern)` compiles a `RegExp`;
`Filter.matchesRegExp(field, RegExp(pattern, caseSensitive: false))` for
options. The field must be a `String` to match.
* Lists: `Filter.arrayContains(field, value)`, `arrayContainsAny(field,
values)`, `arrayContainsAll(field, values)`. `Filter.equals(field, value,
anyInList: true)` and `matches(..., anyInList: true)` match when any list
item matches.
* Combine with `a & b`, `a | b`, or `Filter.and([...])`, `Filter.or([...])`,
`Filter.not(f)` for more than two.
* `Filter.byKey(key)` filters on the record key (`Field.key`); prefer
`store.record(key)` when you have the key.
* `Filter.custom((snapshot) => bool)` runs Dart code per record. Read only,
never modify the snapshot.
* Comparisons use sembast value ordering (`valuesCompare`): numbers,
strings, `Timestamp` and `Blob` compare naturally; a missing or `null`
field never satisfies a range filter. Compare a `Timestamp` field with a
`Timestamp` value, never with a `DateTime`.
### Field paths
* `'a.b.c'` reaches nested maps, `'tags.0'` a list index, `'items.@.tag'`
any item of a list (`Filter.equals('tags.@', 'x')` equals
`Filter.equals('tags', 'x', anyInList: true)`). Escape a literal dot with
`FieldKey.escape('my.key')`.
* `Field.key` (`'_key'`) sorts or filters by key. `Field.value` (`'_value'`)
is the whole value, for stores whose values are not maps
(`Filter.greaterThan(Field.value, 'hi')` on a `StoreRef<int, String>`).
### Sorting and paging
* `SortOrder(field, [ascending = true, nullLast = false])`. Several
`sortOrders` give secondary ordering. `SortOrder.custom(field, compare)`
applies your own comparator to that field's values (`SortOrder<T>` types
the compared values).
* Without `sortOrders` results are in key order (insertion order for
auto-increment int keys). `SortOrder(Field.key, false)` gives newest first.
* `limit` and `offset` page after sorting.
* Cursor paging: `Boundary(values: [...])` (one value per sort order) or
`Boundary(record: lastSnapshot)` as `start`/`end`, exclusive unless
`include: true`. Boundaries require `sortOrders`.
### Listening for changes
* `record.onSnapshot(db)` emits the current snapshot (or null) then again on
every change; `records.onSnapshots(db)` for several keys.
* `store.query(finder: f).onSnapshots(db)` emits the full matching list
after each commit that affects it; `onSnapshot(db)` the first match or
null; `onKeys`/`onKey` the keys; `onCount(db)` the count.
* These streams are single-subscription and take a `Database` (never a
`Transaction`). Always keep the `StreamSubscription` and `cancel()` it
(in `dispose`) or memory leaks. Closing the database ends them.
* In Flutter feed them to `StreamBuilder`. Keep the `QueryRef` in a field
or a `final` so the stream is created once, not on each `build`.
* For reacting inside the writing transaction (triggers) use
`store.addOnChangesListener` (see `sembast-crud`).
### Synchronous API
* Reads have `Sync` variants that return directly: `record.getSync(client)`,
`getSnapshotSync`, `existsSync`, `records.getSync`, `store.findSync`,
`findFirstSync`, `findKeysSync`, `findKeySync`, `countSync`,
`query.getSnapshotsSync`, `getSnapshotSync`, `getKeysSync`, `countSync`,
and `onSnapshotSync`/`onSnapshotsSync`/`onCountSync` streams whose first
event is delivered synchronously.
* Use them for key lookups and small stores (initial UI state). Large sorted
or filtered queries block the isolate; prefer the async API, which yields
periodically (the cooperator) to keep the UI responsive.
### No aggregates or joins
* There is no sum/avg/group-by: fetch snapshots and reduce in Dart. There are
no indexes: every query scans the store, which is fine below ~100K records.
* Store denormalized data or run a second query for related records.
## Examples
### Filter, sort and page
```dart
import 'package:sembast/sembast.dart';
final products = intMapStoreFactory.store('product');
Future<List<RecordSnapshot<int, Map<String, Object?>>>> cheapFirst(
Database db, {
int page = 0,
int pageSize = 20,
}) {
var finder = Finder(
filter: Filter.lessThan('price', 100) & Filter.notNull('name'),
sortOrders: [SortOrder('price'), SortOrder('name')],
limit: pageSize,
offset: page * pageSize,
);
return products.find(db, finder: finder);
}
```
### Text search across fields within a date range
```dart
import 'package:sembast/sembast.dart';
import 'package:sembast/timestamp.dart';
final invoices = intMapStoreFactory.store('invoice');
Future<List<Map<String, Object?>>> search(
Database db,
String text,
int year,
) async {
var regExp = RegExp(RegExp.escape(text), caseSensitive: false);
var filter = Filter.and([
Filter.or([
Filter.matchesRegExp('clientName', regExp),
Filter.matchesRegExp('companyName', regExp),
]),
Filter.greaterThanOrEquals('date', Timestamp.fromDateTime(DateTime(year))),
Filter.lessThan('date', Timestamp.fromDateTime(DateTime(year + 1))),
]);
var snapshots = await invoices.find(
db,
finder: Finder(filter: filter, sortOrders: [SortOrder('date', false)]),
);
return snapshots.values.toList();
}
```
### Nested and list fields
```dart
import 'package:sembast/sembast.dart';
final items = intMapStoreFactory.store('items');
Future<void> nested(Database db) async {
await items.addAll(db, [
{
'name': 'Lamp',
'product': {'code': '1F8'},
'tags': ['light', 'wood'],
'attributes': [
{'tag': 'furniture'},
{'tag': 'plastic'},
],
},
]);
var byCode = await items.findFirst(
db,
finder: Finder(filter: Filter.equals('product.code', '1F8')),
);
print(byCode?['product.code']); // 1F8
var wood = await items.find(
db,
finder: Finder(filter: Filter.arrayContains('tags', 'wood')),
);
var furniture = await items.find(
db,
finder: Finder(filter: Filter.equals('attributes.@.tag', 'furniture')),
);
var sortedByFirstTag = await items.find(
db,
finder: Finder(sortOrders: [SortOrder('tags.0')]),
);
print('${wood.length} ${furniture.length} ${sortedByFirstTag.length}');
}
```
### Last inserted record and key-based queries
```dart
import 'package:sembast/sembast.dart';
final logs = intMapStoreFactory.store('log');
Future<RecordSnapshot<int, Map<String, Object?>>?> lastLog(Database db) =>
logs.findFirst(
db,
finder: Finder(sortOrders: [SortOrder(Field.key, false)]),
);
Future<int> countErrors(Database db) =>
logs.query(finder: Finder(filter: Filter.equals('level', 'error')))
.count(db);
Future<int> purgeOldLogs(Database db, int maxKey) => logs
.query(finder: Finder(filter: Filter.lessThan(Field.key, maxKey)))
.delete(db);
```
### Cursor paging with Boundary
```dart
import 'package:sembast/sembast.dart';
final shop = intMapStoreFactory.store('shop');
/// Returns the page after [last] (or the first page when null).
Future<List<RecordSnapshot<int, Map<String, Object?>>>> nextPage(
Database db, {
RecordSnapshot<int, Map<String, Object?>>? last,
int pageSize = 10,
}) {
var finder = Finder(
sortOrders: [SortOrder('price'), SortOrder('name')],
start: last == null ? null : Boundary(record: last),
limit: pageSize,
);
return shop.find(db, finder: finder);
}
// Equivalent explicit boundary: Boundary(values: [10, 'Chair']) skips
// everything up to price 10 / name 'Chair'.
```
### Listen to a query and a single record
```dart
import 'dart:async';
import 'package:sembast/sembast.dart';
final todos = intMapStoreFactory.store('todo');
class TodoWatcher {
TodoWatcher(this.db);
final Database db;
late final StreamSubscription<List<RecordSnapshot<int, Map<String, Object?>>>>
_listSubscription;
late final StreamSubscription<RecordSnapshot<int, Map<String, Object?>>?>
_recordSubscription;
void start(int watchedKey) {
var query = todos.query(
finder: Finder(
filter: Filter.equals('done', false),
sortOrders: [SortOrder('createdAt')],
),
);
// Emits the current list, then again after every relevant commit.
_listSubscription = query.onSnapshots(db).listen((snapshots) {
print('${snapshots.length} pending todos');
});
// null when the record does not exist or gets deleted.
_recordSubscription =
todos.record(watchedKey).onSnapshot(db).listen((snapshot) {
print('todo $watchedKey: ${snapshot?.value}');
});
}
Future<void> dispose() async {
await _listSubscription.cancel();
await _recordSubscription.cancel();
}
}
```
### Flutter StreamBuilder over a query
```dart
// Flutter widget sketch (needs package:flutter).
Ver en GitHub