mirror of https://github.com/AMT-Cheif/drift.git
Add `RETURNING` variants of update and delete
This commit is contained in:
parent
d79c7e07ba
commit
080ef7210c
|
@ -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
|
||||
|
||||
|
|
|
@ -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)];
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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', () {
|
||||
|
|
|
@ -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',
|
||||
),
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue