fix: Add proper suppor to the noReturn parameter on the ORM (#92)

This commit is contained in:
Marcelo Mendonça Soares 2026-07-03 14:03:44 -03:00 • committed by GitHub
commit 65832eda0a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 308 additions and 9 deletions

View file

@ -323,20 +323,37 @@ class CrdtDatabase implements Database {
}) async { }) async {
if (rows.isEmpty) return []; if (rows.isEmpty) return [];
await _ensureInitialized(); await _ensureInitialized();
if (!_recorder.isCrdtTracked<T>(rows.first.table)) {
return _delegate.insert<T>(
rows,
transaction: transaction,
ignoreConflicts: ignoreConflicts,
noReturn: noReturn,
);
}
return DatabaseUtil.runInTransactionOrSavepoint( return DatabaseUtil.runInTransactionOrSavepoint(
_delegate, _delegate,
transaction, transaction,
(tx) async { (tx) async {
final prepared = _prepareRowsForInsert(rows, tx); final prepared = _prepareRowsForInsert(rows, tx);
// Tracked rows can only skip RETURNING when the prepared rows are
// exactly what gets inserted: every id is caller-provided and a
// conflict throws instead of silently dropping rows.
final skipReturn =
noReturn && !ignoreConflicts && !rows.any((row) => row.id == null);
final result = await _delegate.insert<T>( final result = await _delegate.insert<T>(
prepared.rows, prepared.rows,
transaction: tx, transaction: tx,
ignoreConflicts: ignoreConflicts, ignoreConflicts: ignoreConflicts,
noReturn: skipReturn,
); );
await _recorder.afterInsert<T>(result, tx); final insertedRows = skipReturn ? prepared.rows : result;
_stripStampedRows(result, prepared); await _recorder.afterInsert<T>(insertedRows, tx);
return result; if (noReturn) return <T>[];
_stripStampedRows(insertedRows, prepared);
return insertedRows;
}, },
); );
} }
@ -417,6 +434,14 @@ class CrdtDatabase implements Database {
}) async { }) async {
if (rows.isEmpty) return []; if (rows.isEmpty) return [];
await _ensureInitialized(); await _ensureInitialized();
if (!_recorder.isCrdtTracked<T>(rows.first.table)) {
return _delegate.update<T>(
rows,
columns: columns,
transaction: transaction,
noReturn: noReturn,
);
}
return DatabaseUtil.runInTransactionOrSavepoint( return DatabaseUtil.runInTransactionOrSavepoint(
_delegate, _delegate,
transaction, transaction,
@ -432,7 +457,7 @@ class CrdtDatabase implements Database {
]; ];
await _recorder.afterUpdate(updatedRows, columns, tx); await _recorder.afterUpdate(updatedRows, columns, tx);
return updatedRows; return noReturn ? <T>[] : updatedRows;
}, },
); );
} }
@ -530,10 +555,23 @@ class CrdtDatabase implements Database {
bool noReturn = false, bool noReturn = false,
}) async { }) async {
await _ensureInitialized(); await _ensureInitialized();
if (_recorder.isCrdtTracked<T>()) { if (!_recorder.isCrdtTracked<T>()) {
_assertNoScopeIdColumnValues<T>(columnValues); return _delegate.updateWhere<T>(
columnValues: columnValues,
where: where,
limit: limit,
offset: offset,
orderBy: orderBy,
orderByList: orderByList,
// Remove this once the deprecated member is removed.
// ignore: deprecated_member_use
orderDescending: orderDescending,
transaction: transaction,
noReturn: noReturn,
);
} }
_assertNoScopeIdColumnValues<T>(columnValues);
return DatabaseUtil.runInTransactionOrSavepoint( return DatabaseUtil.runInTransactionOrSavepoint(
_delegate, _delegate,
transaction, transaction,
@ -558,9 +596,8 @@ class CrdtDatabase implements Database {
final columns = columnValues.map((e) => e.column).toList(); final columns = columnValues.map((e) => e.column).toList();
await _recorder.afterUpdate(result, columns, tx); await _recorder.afterUpdate(result, columns, tx);
if (_recorder.isCrdtTracked<T>()) { if (noReturn) return <T>[];
result.forEach(_stripScopeId); result.forEach(_stripScopeId);
}
return result; return result;
}, },
); );
@ -587,6 +624,7 @@ class CrdtDatabase implements Database {
// ignore: deprecated_member_use // ignore: deprecated_member_use
orderDescending: orderDescending, orderDescending: orderDescending,
transaction: transaction, transaction: transaction,
noReturn: noReturn,
); );
} }
@ -629,6 +667,7 @@ class CrdtDatabase implements Database {
// ignore: deprecated_member_use // ignore: deprecated_member_use
orderDescending: orderDescending, orderDescending: orderDescending,
transaction: transaction, transaction: transaction,
noReturn: noReturn,
); );
} }
@ -652,6 +691,7 @@ class CrdtDatabase implements Database {
); );
await _recorder.insteadOfDelete<T>(rows, tx); await _recorder.insteadOfDelete<T>(rows, tx);
if (noReturn) return <T>[];
return _stripScopeIdFromScopedRead(rows, null, tx); return _stripScopeIdFromScopedRead(rows, null, tx);
}, },
); );

View file

@ -56,6 +56,62 @@ void main() {
expect(deletedPerson.scopeId, isNull); expect(deletedPerson.scopeId, isNull);
}); });
}); });
group('when deleting the Person with delete and noReturn,', () {
late List<Person> deleted;
setUp(() async {
deleted = await session.db.transactionForUser(
testCrdtUserId,
(tx) => Person.db.delete(
session,
[person],
transaction: tx,
noReturn: true,
),
);
});
test('then an empty list is returned.', () async {
expect(deleted, isEmpty);
});
test('then a CRDT tombstone is created for the row.', () async {
final tombstone = await CrdtDataDeleted.db.findFirstRow(
session,
where: (t) => t.row.uuidRowId.equals(person.id),
);
expect(tombstone?.isDeleted, isTrue);
});
});
group('when deleting the Person with deleteWhere and noReturn,', () {
late List<Person> deleted;
setUp(() async {
deleted = await session.db.transactionForUser(
testCrdtUserId,
(tx) => Person.db.deleteWhere(
session,
where: (t) => t.id.equals(person.id),
transaction: tx,
noReturn: true,
),
);
});
test('then an empty list is returned.', () async {
expect(deleted, isEmpty);
});
test('then a CRDT tombstone is created for the row.', () async {
final tombstone = await CrdtDataDeleted.db.findFirstRow(
session,
where: (t) => t.row.uuidRowId.equals(person.id),
);
expect(tombstone?.isDeleted, isTrue);
});
});
}); });
group('Given a person table with a deleted row,', () { group('Given a person table with a deleted row,', () {

View file

@ -172,6 +172,79 @@ void main() {
expect(crdt.map((e) => e.uuidRowId).toSet(), insertedIds); expect(crdt.map((e) => e.uuidRowId).toSet(), insertedIds);
}); });
}); });
group('when inserting a Person with a caller-provided id and noReturn,', () {
late UuidValue id;
late List<Person> inserted;
setUp(() async {
id = const Uuid().v7obj();
inserted = await session.db.transactionForUser(
testCrdtUserId,
(tx) => Person.db.insert(
session,
[Person(id: id, name: 'fixed')],
transaction: tx,
noReturn: true,
),
);
});
test('then an empty list is returned.', () async {
expect(inserted, isEmpty);
});
test('then the row exists in the person table.', () async {
final row = await Person.db.findById(session, id);
expect(row?.name, 'fixed');
});
test('then CRDT metadata is recorded for the row.', () async {
final crdtRow = await CrdtDataRow.db.findFirstRow(
session,
where: (t) => t.uuidRowId.equals(id),
);
expect(crdtRow, isNotNull);
});
});
group('when inserting a Person without a caller-provided id and noReturn,', () {
late List<Person> inserted;
late Person? row;
setUp(() async {
inserted = await session.db.transactionForUser(
testCrdtUserId,
(tx) => Person.db.insert(
session,
[Person(name: 'generated')],
transaction: tx,
noReturn: true,
),
);
row = await Person.db.findFirstRow(
session,
where: (t) => t.name.equals('generated'),
);
});
test('then an empty list is returned.', () async {
expect(inserted, isEmpty);
});
test('then the row exists in the person table.', () async {
expect(row, isNotNull);
});
test('then CRDT metadata is recorded for the row.', () async {
final crdtRow = await CrdtDataRow.db.findFirstRow(
session,
where: (t) => t.uuidRowId.equals(row!.id),
);
expect(crdtRow, isNotNull);
});
});
}); });
group('Given a person row with a fixed id,', () { group('Given a person row with a fixed id,', () {

View file

@ -194,6 +194,67 @@ void main() {
); );
}, },
); );
group('when updating the Person with update and noReturn,', () {
late List<Person> updated;
setUp(() async {
updated = await session.db.transactionForUser(
testCrdtUserId,
(tx) => Person.db.update(
session,
[person.copyWith(name: 'updated')],
columns: (t) => [t.name],
transaction: tx,
noReturn: true,
),
);
});
test('then an empty list is returned.', () async {
expect(updated, isEmpty);
});
test('then the person row reflects the new values.', () async {
final row = await Person.db.findById(session, person.id!);
expect(row?.name, 'updated');
});
test('then a CRDT field is created for the name column.', () async {
final field = await CrdtDataField.db.findFirstRow(
session,
where: (t) => t.row.uuidRowId.equals(person.id),
include: CrdtDataField.include(column: CrdtSchemaColumn.include()),
);
expect(field?.column?.name, 'name');
});
});
group('when updating the Person with updateWhere and noReturn,', () {
late List<Person> updated;
setUp(() async {
updated = await session.db.transactionForUser(
testCrdtUserId,
(tx) => Person.db.updateWhere(
session,
columnValues: (t) => [t.name('updated')],
where: (t) => t.id.equals(person.id),
transaction: tx,
noReturn: true,
),
);
});
test('then an empty list is returned.', () async {
expect(updated, isEmpty);
});
test('then the person row reflects the new values.', () async {
final row = await Person.db.findById(session, person.id!);
expect(row?.name, 'updated');
});
});
}); });
group('Given a person table with a deleted row,', () { group('Given a person table with a deleted row,', () {
@ -513,4 +574,73 @@ void main() {
}); });
}); });
}); });
group('Given a row on a table not tracked by CRDT,', () {
late CrdtSyncIntegrityViolation violation;
setUp(() async {
violation = await CrdtSyncIntegrityViolation.db.insertRow(
session,
CrdtSyncIntegrityViolation(
type: CrdtSyncViolationType.ownershipCollision,
domainTableName: Person.t.tableName,
uuidRowId: const Uuid().v7obj(),
incomingScopeUuid: testCrdtUserId,
operation: CrdtSyncViolationOperation.mergeInsert,
firstSeenAt: DateTime.now(),
lastSeenAt: DateTime.now(),
occurrences: 1,
),
);
});
group('when updating the row with update and noReturn,', () {
late List<CrdtSyncIntegrityViolation> updated;
setUp(() async {
updated = await CrdtSyncIntegrityViolation.db.update(
session,
[violation.copyWith(occurrences: 2)],
noReturn: true,
);
});
test('then an empty list is returned.', () async {
expect(updated, isEmpty);
});
test('then the row reflects the new values.', () async {
final row = await CrdtSyncIntegrityViolation.db.findById(
session,
violation.id!,
);
expect(row?.occurrences, 2);
});
});
group('when updating the row with updateWhere and noReturn,', () {
late List<CrdtSyncIntegrityViolation> updated;
setUp(() async {
updated = await CrdtSyncIntegrityViolation.db.updateWhere(
session,
columnValues: (t) => [t.occurrences(3)],
where: (t) => t.id.equals(violation.id),
noReturn: true,
);
});
test('then an empty list is returned.', () async {
expect(updated, isEmpty);
});
test('then the row reflects the new values.', () async {
final row = await CrdtSyncIntegrityViolation.db.findById(
session,
violation.id!,
);
expect(row?.occurrences, 3);
});
});
});
} }