Add `RETURNING` variants of update and delete

This commit is contained in:
Simon Binder 2022-07-25 22:27:57 +02:00
parent d79c7e07ba
commit 080ef7210c
No known key found for this signature in database
GPG Key ID: 7891917E4147B8C0
6 changed files with 135 additions and 7 deletions

View File

@ -17,10 +17,13 @@
- __Breaking__: Remove the `includeJoinedTableColumns` parameter on `selectOnly()`.
The method now behaves as if that parameter was turned off. To use columns from a
joined table, add them with `addColumns`.
- Add support for storing date times as (ISO-8601) strings. For details on how
to use this, see [the documentation](https://drift.simonbinder.eu/docs/getting-started/advanced_dart_tables/#supported-column-types).
- Consistently handle transaction errors like a failing `BEGIN` or `COMMIT`
across database implementations.
- Add `writeReturning` to update statements; `deleteReturning` and `goAndReturn`
to delete statatements.
- Support nested transactions.
- Fix nullability of `min`, `max` and `avg` in the Dart query builder.
## 1.7.1

View File

@ -13,21 +13,34 @@ class DeleteStatement<T extends Table, D> extends Query<T, D>
ctx.buffer.write('DELETE FROM ${table.tableWithAlias}');
}
/// Deletes just this entity. May not be used together with [where].
///
/// Returns the amount of rows that were deleted by this statement directly
/// (not including additional rows that might be affected through triggers or
/// foreign key constraints).
Future<int> delete(Insertable<D> entity) {
void _prepareDeleteOne(Insertable<D> entity) {
assert(
whereExpr == null,
'When deleting an entity, you may not use where(...)'
'as well. The where clause will be determined automatically');
whereSamePrimaryKey(entity);
}
/// Deletes just this entity. May not be used together with [where].
///
/// Returns the amount of rows that were deleted by this statement directly
/// (not including additional rows that might be affected through triggers or
/// foreign key constraints).
Future<int> delete(Insertable<D> entity) {
_prepareDeleteOne(entity);
return go();
}
/// Like [delete], but returns the deleted row from the database.
///
/// If no matching row with the same primary key exists, `null` is returned.
Future<D?> deleteReturning(Insertable<D> entity) async {
_prepareDeleteOne(entity);
writeReturningClause = true;
return (await _goReturning()).singleOrNull;
}
/// Deletes all rows matched by the set [where] clause and the optional
/// limit.
///
@ -47,4 +60,25 @@ class DeleteStatement<T extends Table, D> extends Query<T, D>
return rows;
});
}
/// Like [go], but it also returns all rows affected by this delete operation.
Future<List<D>> goAndReturn() {
writeReturningClause = true;
return _goReturning();
}
Future<List<D>> _goReturning() async {
final ctx = constructQuery();
return ctx.executor!.doWhenOpened((e) async {
final rows = await e.runSelect(ctx.sql, ctx.boundVariables);
if (rows.isNotEmpty) {
database.notifyUpdates(
{TableUpdate.onTable(_sourceTable, kind: UpdateKind.delete)});
}
return [for (final rawRow in rows) table.map(rawRow)];
});
}
}

View File

@ -26,6 +26,10 @@ abstract class Query<T extends HasResultSet, D> extends Component {
@protected
Limit? limitExpr;
/// Whether a `RETURNING *` clause should be added to this statement.
@protected
bool writeReturningClause = false;
GroupBy? _groupBy;
/// Subclasses must override this and write the part of the statement that
@ -53,6 +57,12 @@ abstract class Query<T extends HasResultSet, D> extends Component {
writeWithSpace(_groupBy);
writeWithSpace(orderByExpr);
writeWithSpace(limitExpr);
if (writeReturningClause) {
if (needsWhitespace) context.writeWhitespace();
context.buffer.write('RETURNING *');
}
}
/// Constructs the query that can then be sent to the database executor.

View File

@ -76,6 +76,28 @@ class UpdateStatement<T extends Table, D> extends Query<T, D>
return await _performQuery();
}
/// Applies the updates from [entity] to all rows matching the applied `where`
/// clause and returns affected rows _after the update_.
///
/// For more details on writing entries, see [write].
/// Note that this requires sqlite 3.35 or later.
Future<List<D>> writeReturning(Insertable<D> entity) async {
writeReturningClause = true;
await write(entity, dontExecute: true);
final ctx = constructQuery();
final rows = await ctx.executor!.doWhenOpened((e) {
return e.runSelect(ctx.sql, ctx.boundVariables);
});
if (rows.isNotEmpty) {
database.notifyUpdates(
{TableUpdate.onTable(_sourceTable, kind: UpdateKind.update)});
}
return [for (final rawRow in rows) table.map(rawRow)];
}
/// Replaces the old version of [entity] that is stored in the database with
/// the fields of the [entity] provided here. This implicitly applies a
/// [where] clause to rows with the same primary key as [entity], so that only

View File

@ -46,6 +46,34 @@ void main() {
const [3, 2],
));
});
group('RETURNING', () {
test('for one row', () async {
when(executor.runSelect(any, any)).thenAnswer((_) {
return Future.value([
{'id': 10, 'content': 'Content'}
]);
});
final returnedValue = await db
.delete(db.todosTable)
.deleteReturning(const TodosTableCompanion(id: Value(10)));
verify(executor
.runSelect('DELETE FROM todos WHERE id = ? RETURNING *;', [10]));
verify(streamQueries.handleTableUpdates(
{TableUpdate.onTable(db.todosTable, kind: UpdateKind.delete)}));
expect(returnedValue, const TodoEntry(id: 10, content: 'Content'));
});
test('for multiple rows', () async {
final rows = await db.delete(db.users).goAndReturn();
expect(rows, isEmpty);
verify(executor.runSelect('DELETE FROM users RETURNING *;', []));
verifyNever(streamQueries.handleTableUpdates(any));
});
});
});
group('executes DELETE statements', () {

View File

@ -198,4 +198,35 @@ void main() {
['new name', 3]));
});
});
test('RETURNING', () async {
when(executor.runSelect(any, any)).thenAnswer((_) {
return Future.value([
{
'id': 3,
'desc': 'test',
'priority': 0,
'description_in_upper_case': 'TEST',
},
]);
});
final rows = await db.categories
.update()
.writeReturning(const CategoriesCompanion(description: Value('test')));
verify(executor
.runSelect('UPDATE categories SET "desc" = ? RETURNING *;', ['test']));
verify(streamQueries.handleTableUpdates(
{TableUpdate.onTable(db.categories, kind: UpdateKind.update)}));
expect(rows, const [
Category(
id: 3,
description: 'test',
priority: CategoryPriority.low,
descriptionInUpperCase: 'TEST',
),
]);
});
}