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.
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
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
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
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
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
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
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
// Flutter widget sketch (needs package:flutter).
// final query = store.query(finder: Finder(sortOrders: [SortOrder('name')]));
//
// StreamBuilder<List<RecordSnapshot<int, Map<String, Object?>>>>(
// stream: query.onSnapshots(db), // created once, in a field or initState
// builder: (context, snapshot) {
// var items = snapshot.data ?? [];
// return ListView(
// children: [for (var item in items) Text('${item['name']}')],
// );
// },
// )
Custom filter and custom sort
import 'package:sembast/sembast.dart';
final people = intMapStoreFactory.store('people');
Future<List<RecordSnapshot<int, Map<String, Object?>>>> adultsByLastName(
Database db,
) {
var finder = Finder(
filter: Filter.custom((snapshot) => (snapshot['age'] as int? ?? 0) >= 18),
sortOrders: [
SortOrder<String>.custom(
'name',
(a, b) => a.split(' ').last.compareTo(b.split(' ').last),
),
],
);
return people.find(db, finder: finder);
}
Synchronous read for initial state
import 'package:sembast/sembast.dart';
final settings = StoreRef<String, Object>.main();
/// Cheap key lookup, safe to call synchronously (for example in a getter).
bool darkMode(Database db) =>
settings.record('darkMode').getSync(db) as bool? ?? false;
Common mistakes
- Passing a
Finder to store.count(db, filter: ...); it takes a Filter.
Use store.query(finder: f).count(db) for a full finder.
- Listening with a
Transaction (onSnapshots(txn) does not compile); the
streams take a Database.
- Forgetting to cancel
onSnapshot/onSnapshots subscriptions, or creating
a new stream in every build.
- Comparing a
Timestamp field with a DateTime value: no record matches.
- Using
Boundary without sortOrders.
- Expecting
Filter.matches to work on non-string fields (it does not
match) or forgetting RegExp.escape on user input.
- Running big sorted queries with the
Sync API on the UI isolate.
More
See references/filters.md for the complete Filter,
SortOrder, Boundary and QueryRef listings and the value ordering rules.
1---2name: sembast-query3description: 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.4---56# sembast: queries, filters, sorting and listeners78Sembast keeps the whole database in memory, so queries are in-memory9filtering and sorting of a store. A `Finder` describes the query; run it once10with `store.find` / `store.findFirst` / `store.query(finder: f)`, or subscribe11to results with `onSnapshot(s)` streams that re-emit after each commit.1213```dart14import 'package:sembast/sembast.dart';1516final animals = intMapStoreFactory.store('animals');1718Future<void> demo(Database db) async {19 var finder = Finder(20 filter: Filter.greaterThan('age', 2) & Filter.equals('kind', 'cat'),21 sortOrders: [SortOrder('name')],22 limit: 20,23 );24 var snapshots = await animals.find(db, finder: finder);25 for (var snapshot in snapshots) {26 print('${snapshot.key}: ${snapshot['name']}');27 }28}29```3031## Guidelines3233### Reading by key3435* Fastest path: `store.record(key).get(client)` (value or null),36 `getSnapshot(client)`, `exists(client)`; several keys with37 `store.records(keys).get(client)` (a list with `null` for missing keys).38* A `RecordSnapshot` has `ref`, `key`, `value` and `operator [](field)` which39 resolves dotted paths (`snapshot['address.city']`), `Field.key` and40 `Field.value`. On an `Iterable<RecordSnapshot>` use `.keys`, `.values` and41 `.keysAndValues`.42* Snapshot values are read-only; clone with `cloneMap` from43 `package:sembast/utils/value_utils.dart` before editing.4445### Finder4647* `Finder({filter, sortOrders, limit, offset, start, end})`. Everything is48 optional; an empty `Finder()` matches every record. Order of application:49 filter, sort, boundaries, offset, limit.50* `store.find(client, finder: f)` returns `List<RecordSnapshot>`;51 `findFirst` the first or null; `findKeys` / `findKey` only keys;52 `store.count(client, filter: filter)` takes a `Filter`, not a `Finder`.53* `store.query(finder: f)` returns a reusable `QueryRef` with54 `getSnapshots`, `getSnapshot`, `getKeys`, `getKey`, `count`, `delete` (all55 taking a `DatabaseClient`) and the streams below (taking a `Database`).56* `store.stream(client, filter: filter)` iterates records unsorted without57 building a list; use it for one pass over a large store.5859### Filters6061* Field comparison: `Filter.equals(field, value)`, `notEquals`, `isNull`,62 `notNull`, `lessThan`, `lessThanOrEquals`, `greaterThan`,63 `greaterThanOrEquals`, `inList(field, list)`.64* Text: `Filter.matches(field, pattern)` compiles a `RegExp`;65 `Filter.matchesRegExp(field, RegExp(pattern, caseSensitive: false))` for66 options. The field must be a `String` to match.67* Lists: `Filter.arrayContains(field, value)`, `arrayContainsAny(field,68 values)`, `arrayContainsAll(field, values)`. `Filter.equals(field, value,69 anyInList: true)` and `matches(..., anyInList: true)` match when any list70 item matches.71* Combine with `a & b`, `a | b`, or `Filter.and([...])`, `Filter.or([...])`,72 `Filter.not(f)` for more than two.73* `Filter.byKey(key)` filters on the record key (`Field.key`); prefer74 `store.record(key)` when you have the key.75* `Filter.custom((snapshot) => bool)` runs Dart code per record. Read only,76 never modify the snapshot.77* Comparisons use sembast value ordering (`valuesCompare`): numbers,78 strings, `Timestamp` and `Blob` compare naturally; a missing or `null`79 field never satisfies a range filter. Compare a `Timestamp` field with a80 `Timestamp` value, never with a `DateTime`.8182### Field paths8384* `'a.b.c'` reaches nested maps, `'tags.0'` a list index, `'items.@.tag'`85 any item of a list (`Filter.equals('tags.@', 'x')` equals86 `Filter.equals('tags', 'x', anyInList: true)`). Escape a literal dot with87 `FieldKey.escape('my.key')`.88* `Field.key` (`'_key'`) sorts or filters by key. `Field.value` (`'_value'`)89 is the whole value, for stores whose values are not maps90 (`Filter.greaterThan(Field.value, 'hi')` on a `StoreRef<int, String>`).9192### Sorting and paging9394* `SortOrder(field, [ascending = true, nullLast = false])`. Several95 `sortOrders` give secondary ordering. `SortOrder.custom(field, compare)`96 applies your own comparator to that field's values (`SortOrder<T>` types97 the compared values).98* Without `sortOrders` results are in key order (insertion order for99 auto-increment int keys). `SortOrder(Field.key, false)` gives newest first.100* `limit` and `offset` page after sorting.101* Cursor paging: `Boundary(values: [...])` (one value per sort order) or102 `Boundary(record: lastSnapshot)` as `start`/`end`, exclusive unless103 `include: true`. Boundaries require `sortOrders`.104105### Listening for changes106107* `record.onSnapshot(db)` emits the current snapshot (or null) then again on108 every change; `records.onSnapshots(db)` for several keys.109* `store.query(finder: f).onSnapshots(db)` emits the full matching list110 after each commit that affects it; `onSnapshot(db)` the first match or111 null; `onKeys`/`onKey` the keys; `onCount(db)` the count.112* These streams are single-subscription and take a `Database` (never a113 `Transaction`). Always keep the `StreamSubscription` and `cancel()` it114 (in `dispose`) or memory leaks. Closing the database ends them.115* In Flutter feed them to `StreamBuilder`. Keep the `QueryRef` in a field116 or a `final` so the stream is created once, not on each `build`.117* For reacting inside the writing transaction (triggers) use118 `store.addOnChangesListener` (see `sembast-crud`).119120### Synchronous API121122* Reads have `Sync` variants that return directly: `record.getSync(client)`,123 `getSnapshotSync`, `existsSync`, `records.getSync`, `store.findSync`,124 `findFirstSync`, `findKeysSync`, `findKeySync`, `countSync`,125 `query.getSnapshotsSync`, `getSnapshotSync`, `getKeysSync`, `countSync`,126 and `onSnapshotSync`/`onSnapshotsSync`/`onCountSync` streams whose first127 event is delivered synchronously.128* Use them for key lookups and small stores (initial UI state). Large sorted129 or filtered queries block the isolate; prefer the async API, which yields130 periodically (the cooperator) to keep the UI responsive.131132### No aggregates or joins133134* There is no sum/avg/group-by: fetch snapshots and reduce in Dart. There are135 no indexes: every query scans the store, which is fine below ~100K records.136* Store denormalized data or run a second query for related records.137138## Examples139140### Filter, sort and page141142```dart143import 'package:sembast/sembast.dart';144145final products = intMapStoreFactory.store('product');146147Future<List<RecordSnapshot<int, Map<String, Object?>>>> cheapFirst(148 Database db, {149 int page = 0,150 int pageSize = 20,151}) {152 var finder = Finder(153 filter: Filter.lessThan('price', 100) & Filter.notNull('name'),154 sortOrders: [SortOrder('price'), SortOrder('name')],155 limit: pageSize,156 offset: page * pageSize,157 );158 return products.find(db, finder: finder);159}160```161162### Text search across fields within a date range163164```dart165import 'package:sembast/sembast.dart';166import 'package:sembast/timestamp.dart';167168final invoices = intMapStoreFactory.store('invoice');169170Future<List<Map<String, Object?>>> search(171 Database db,172 String text,173 int year,174) async {175 var regExp = RegExp(RegExp.escape(text), caseSensitive: false);176 var filter = Filter.and([177 Filter.or([178 Filter.matchesRegExp('clientName', regExp),179 Filter.matchesRegExp('companyName', regExp),180 ]),181 Filter.greaterThanOrEquals('date', Timestamp.fromDateTime(DateTime(year))),182 Filter.lessThan('date', Timestamp.fromDateTime(DateTime(year + 1))),183 ]);184 var snapshots = await invoices.find(185 db,186 finder: Finder(filter: filter, sortOrders: [SortOrder('date', false)]),187 );188 return snapshots.values.toList();189}190```191192### Nested and list fields193194```dart195import 'package:sembast/sembast.dart';196197final items = intMapStoreFactory.store('items');198199Future<void> nested(Database db) async {200 await items.addAll(db, [201 {202 'name': 'Lamp',203 'product': {'code': '1F8'},204 'tags': ['light', 'wood'],205 'attributes': [206 {'tag': 'furniture'},207 {'tag': 'plastic'},208 ],209 },210 ]);211 var byCode = await items.findFirst(212 db,213 finder: Finder(filter: Filter.equals('product.code', '1F8')),214 );215 print(byCode?['product.code']); // 1F8216217 var wood = await items.find(218 db,219 finder: Finder(filter: Filter.arrayContains('tags', 'wood')),220 );221 var furniture = await items.find(222 db,223 finder: Finder(filter: Filter.equals('attributes.@.tag', 'furniture')),224 );225 var sortedByFirstTag = await items.find(226 db,227 finder: Finder(sortOrders: [SortOrder('tags.0')]),228 );229 print('${wood.length} ${furniture.length} ${sortedByFirstTag.length}');230}231```232233### Last inserted record and key-based queries234235```dart236import 'package:sembast/sembast.dart';237238final logs = intMapStoreFactory.store('log');239240Future<RecordSnapshot<int, Map<String, Object?>>?> lastLog(Database db) =>241 logs.findFirst(242 db,243 finder: Finder(sortOrders: [SortOrder(Field.key, false)]),244 );245246Future<int> countErrors(Database db) =>247 logs.query(finder: Finder(filter: Filter.equals('level', 'error')))248 .count(db);249250Future<int> purgeOldLogs(Database db, int maxKey) => logs251 .query(finder: Finder(filter: Filter.lessThan(Field.key, maxKey)))252 .delete(db);253```254255### Cursor paging with Boundary256257```dart258import 'package:sembast/sembast.dart';259260final shop = intMapStoreFactory.store('shop');261262/// Returns the page after [last] (or the first page when null).263Future<List<RecordSnapshot<int, Map<String, Object?>>>> nextPage(264 Database db, {265 RecordSnapshot<int, Map<String, Object?>>? last,266 int pageSize = 10,267}) {268 var finder = Finder(269 sortOrders: [SortOrder('price'), SortOrder('name')],270 start: last == null ? null : Boundary(record: last),271 limit: pageSize,272 );273 return shop.find(db, finder: finder);274}275276// Equivalent explicit boundary: Boundary(values: [10, 'Chair']) skips277// everything up to price 10 / name 'Chair'.278```279280### Listen to a query and a single record281282```dart283import 'dart:async';284285import 'package:sembast/sembast.dart';286287final todos = intMapStoreFactory.store('todo');288289class TodoWatcher {290 TodoWatcher(this.db);291 final Database db;292 late final StreamSubscription<List<RecordSnapshot<int, Map<String, Object?>>>>293 _listSubscription;294 late final StreamSubscription<RecordSnapshot<int, Map<String, Object?>>?>295 _recordSubscription;296297 void start(int watchedKey) {298 var query = todos.query(299 finder: Finder(300 filter: Filter.equals('done', false),301 sortOrders: [SortOrder('createdAt')],302 ),303 );304 // Emits the current list, then again after every relevant commit.305 _listSubscription = query.onSnapshots(db).listen((snapshots) {306 print('${snapshots.length} pending todos');307 });308 // null when the record does not exist or gets deleted.309 _recordSubscription =310 todos.record(watchedKey).onSnapshot(db).listen((snapshot) {311 print('todo $watchedKey: ${snapshot?.value}');312 });313 }314315 Future<void> dispose() async {316 await _listSubscription.cancel();317 await _recordSubscription.cancel();318 }319}320```321322### Flutter StreamBuilder over a query323324```dart325// Flutter widget sketch (needs package:flutter).326// final query = store.query(finder: Finder(sortOrders: [SortOrder('name')]));327//328// StreamBuilder<List<RecordSnapshot<int, Map<String, Object?>>>>(329// stream: query.onSnapshots(db), // created once, in a field or initState330// builder: (context, snapshot) {331// var items = snapshot.data ?? [];332// return ListView(333// children: [for (var item in items) Text('${item['name']}')],334// );335// },336// )337```338339### Custom filter and custom sort340341```dart342import 'package:sembast/sembast.dart';343344final people = intMapStoreFactory.store('people');345346Future<List<RecordSnapshot<int, Map<String, Object?>>>> adultsByLastName(347 Database db,348) {349 var finder = Finder(350 filter: Filter.custom((snapshot) => (snapshot['age'] as int? ?? 0) >= 18),351 sortOrders: [352 SortOrder<String>.custom(353 'name',354 (a, b) => a.split(' ').last.compareTo(b.split(' ').last),355 ),356 ],357 );358 return people.find(db, finder: finder);359}360```361362### Synchronous read for initial state363364```dart365import 'package:sembast/sembast.dart';366367final settings = StoreRef<String, Object>.main();368369/// Cheap key lookup, safe to call synchronously (for example in a getter).370bool darkMode(Database db) =>371 settings.record('darkMode').getSync(db) as bool? ?? false;372```373374## Common mistakes375376* Passing a `Finder` to `store.count(db, filter: ...)`; it takes a `Filter`.377 Use `store.query(finder: f).count(db)` for a full finder.378* Listening with a `Transaction` (`onSnapshots(txn)` does not compile); the379 streams take a `Database`.380* Forgetting to cancel `onSnapshot`/`onSnapshots` subscriptions, or creating381 a new stream in every `build`.382* Comparing a `Timestamp` field with a `DateTime` value: no record matches.383* Using `Boundary` without `sortOrders`.384* Expecting `Filter.matches` to work on non-string fields (it does not385 match) or forgetting `RegExp.escape` on user input.386* Running big sorted queries with the `Sync` API on the UI isolate.387388## More389390See [references/filters.md](references/filters.md) for the complete `Filter`,391`SortOrder`, `Boundary` and `QueryRef` listings and the value ordering rules.