Skip to content
Open
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
26 changes: 25 additions & 1 deletion docs/06-concepts/03-data-and-the-database/02-database/02-crud.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ When using `ignoreConflicts` with models that have [non-persistent fields](./tab

## Read

There are three different read operations available.
There are four different read operations available.

### Finding by id

Expand Down Expand Up @@ -107,6 +107,30 @@ This operation returns a `List` of your models matching the filtering criteria.

See [filter](./filtering) and [sort](./sorting) for all filter and sorting operations and [pagination](pagination) for how to paginate the result.

### Watching rows

To keep a result up to date, use the `watch` method. It takes the same `where`, `orderBy`, `orderByList`, `limit`, `offset`, and `include` parameters as `find`, and returns a `Stream` that emits the matching rows as soon as you listen to it.

```dart
var subscription = Company.db.watch(
session,
where: (t) => t.name.like('%Ltd'),
orderBy: (t) => t.name,
).listen((companies) {
// Handle the latest matching rows.
});
```

The stream runs the query again and emits the result every time a write to one of the watched tables commits, until you cancel the subscription. The watched tables are the model's table and every table that `where`, `orderBy`, `orderByList`, and `include` refer to. Writes inside a [transaction](transactions) emit once it commits, and a rolled-back transaction emits nothing.

To also react to writes on other tables, pass them with `alsoTriggerOnTables`, for example `alsoTriggerOnTables: [Town.t]`. This is needed when a filter reads a table only through raw SQL, which `watch` does not inspect.

By default, the query runs at most once every 30 milliseconds, no matter how many writes happen in between. Pass a different `Duration` as `throttle` to change the interval, or `null` to run the query after every write.

:::note
The `watch` method is only supported on SQLite, which includes the [client-side database](client-side-database). On PostgreSQL, calling it throws an `UnsupportedError`.
:::

## Update

There are multiple update operations available for different use cases.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,27 @@ int result = await session.db.unsafeExecute(
);
```

## `unsafeWatch`

Returns a `Stream` that runs a read query and emits its `DatabaseResult` as soon as you listen to it, then again every time a write to one of the queried tables commits. It is the raw counterpart of the generated [`watch`](crud#watching-rows) method and takes the same `throttle` parameter.

```dart
var subscription = session.db.unsafeWatch(
r'SELECT id, size FROM apparel WHERE color = @color',
parameters: QueryParameters.named({'color': 'green'}),
).listen((result) {
// Handle the latest rows.
});
```

The queried tables are detected automatically. To set them yourself, pass their names with `triggerOnTables`. The list replaces the detection, so include every table the stream should react to.

The method is only supported on SQLite. On PostgreSQL, calling it throws an `UnsupportedError`.

:::warning
A raw `DELETE` without a `WHERE` clause can skip SQLite's change notifications, so streams from `unsafeWatch` and the generated `watch` method don't emit after it. Add a `WHERE` clause, such as `WHERE true`, when clearing a watched table. The generated delete methods always include one.
:::

## `unsafeSimpleQuery`

Similar to `unsafeQuery`, but uses the simple query protocol. This protocol does not support parameter binding, making it more susceptible to SQL injection. **Use with extreme caution and only when absolutely necessary.**
Expand Down Expand Up @@ -84,7 +105,7 @@ var result = await session.db.unsafeQuery(
```

:::danger
Always sanitize your input when using raw query methods. For the `unsafeQuery` and `unsafeExecute` methods, use query parameters to prevent SQL injection. Avoid using `unsafeSimpleQuery` and `unsafeSimpleExecute` unless the simple query protocol is strictly required.
Always sanitize your input when using raw query methods. For the `unsafeQuery`, `unsafeExecute`, and `unsafeWatch` methods, use query parameters to prevent SQL injection. Avoid using `unsafeSimpleQuery` and `unsafeSimpleExecute` unless the simple query protocol is strictly required.
:::

## Working with results
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,35 @@ var companies = await Company.db.find(
- [Vector](vector-and-geography-fields#vector-fields) queries are not supported, and [geography](vector-and-geography-fields#geography-fields) values round-trip through CRUD but throw on spatial query operations.
- Column types and encodings differ from Postgres. See [Field types](../models/field-types#postgres-vs-sqlite).

## Rebuild widgets when data changes

To keep a widget in sync with the database, [watch](crud#watching-rows) a query and pass the stream to a `StreamBuilder`. The builder runs with the current rows, then again after every committed write to the watched tables. This example assumes the widget receives the session as `session`:

```dart
class _CompanyListState extends State<CompanyList> {
// Create the stream once, so rebuilds don't restart the query.
late final _companies = Company.db.watch(
widget.session,
orderBy: (t) => t.name,
);

@override
Widget build(BuildContext context) {
return StreamBuilder(
stream: _companies,
builder: (context, snapshot) {
final companies = snapshot.data ?? [];
return ListView(
children: [for (final company in companies) Text(company.name)],
);
},
);
}
}
```

The `StreamBuilder` cancels its subscription when the widget is removed, which stops the watch.

## Sync tables with the server

:::warning
Expand Down
2 changes: 1 addition & 1 deletion docs/06-concepts/08-testing/02-writing-tests.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ The generated file exports these, so you can name them in a test without importi
| `ServerpodUnauthenticatedException` | An endpoint call was made without authentication and the endpoint requires it. |
| `ServerpodInsufficientAccessException` | The authentication used did not carry the scopes the endpoint requires. |
| `ConnectionClosedException` | A stream connection closed with an error, for example because authentication was revoked. |
| `InvalidConfigurationException` | The endpoint ran database calls or transactions in parallel, which the automatic rollback cannot wrap. See [`rollbackDatabase`](configuration#rollbackdatabase). |
| `InvalidConfigurationException` | Database calls or transactions ran in parallel, or a query was watched, neither of which the automatic rollback can wrap. See [`rollbackDatabase`](configuration#rollbackdatabase). |

The test server also throws when it fails to start or takes longer than the start timeout. That error type is not exported, so you cannot catch it by name, but the message says which of the two happened.

Expand Down
4 changes: 3 additions & 1 deletion docs/06-concepts/08-testing/03-configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ By default each test runs inside a transaction that is rolled back when that tes
| `RollbackDatabase.afterAll` | Once the whole `withServerpod` group finishes. |
| `RollbackDatabase.disabled` | Never. Data stays until the group finishes and its database is dropped. |

Three situations call for changing it.
Four situations call for changing it.

**Tests that build on each other.** When one test depends on what the previous one left behind, `afterAll` keeps state across the group and still rolls everything back at the end. Tests that share state are usually worth avoiding, but this helps when the setup is expensive.

Expand All @@ -85,6 +85,8 @@ Future<void> concurrentTransactionCalls(Session session) async {
}
```

**Watching queries.** The [`watch`](../data-and-the-database/database/crud#watching-rows) and [`unsafeWatch`](../data-and-the-database/database/raw-access#unsafewatch) streams only see committed data, so they cannot observe writes inside the rollback transaction. Both throw `InvalidConfigurationException` unless you pass `RollbackDatabase.disabled`.

With rollback disabled, later tests in the same group see what earlier tests committed. To start each test from a clean state, clean up in `tearDown`:

```dart
Expand Down
Loading