fix: General tests improvements and misc small fixes (#104)

This commit is contained in:
Marcelo Mendonça Soares 2026-07-22 14:28:14 -03:00 • committed by GitHub
commit 8e7e6551e0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 2902 additions and 2155 deletions

View file

@ -336,24 +336,45 @@ class CrdtDatabase implements Database {
transaction,
(tx) async {
final prepared = _prepareRowsForInsert(rows, tx);
final deletedRowIds = await _recorder.deletedRowIds(prepared.rows, tx);
final rowsToInsert = [
for (final row in prepared.rows)
if (!deletedRowIds.contains(row.id)) row,
];
// 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>(
prepared.rows,
transaction: tx,
ignoreConflicts: ignoreConflicts,
noReturn: skipReturn,
);
final result = rowsToInsert.isEmpty
? <T>[]
: await _delegate.insert<T>(
rowsToInsert,
transaction: tx,
ignoreConflicts: ignoreConflicts,
noReturn: skipReturn,
);
final insertedRows = skipReturn ? prepared.rows : result;
final insertedRows = skipReturn ? rowsToInsert : result;
await _recorder.afterInsert<T>(insertedRows, tx);
final reinsertedRows = await _reinsertTombstonedRows(
prepared.rows,
insertedRows,
tx,
deletedRowIds: deletedRowIds,
);
if (noReturn) return <T>[];
_stripStampedRows(insertedRows, prepared);
return insertedRows;
final affectedRows = _orderAffectedRows(
prepared.rows,
insertedRows,
reinsertedRows,
);
_stripStampedRows(affectedRows, prepared);
return affectedRows;
},
);
}
@ -363,30 +384,7 @@ class CrdtDatabase implements Database {
T row, {
Transaction? transaction,
}) async {
await _ensureInitialized();
return DatabaseUtil.runInTransactionOrSavepoint(
_delegate,
transaction,
(tx) async {
final prepared = _prepareRowsForInsert([row], tx);
final databaseRow = prepared.rows.single;
late final T result;
try {
result = await _delegate.insertRow<T>(databaseRow, transaction: tx);
} on DatabaseQueryException {
if (!await _recorder.isDeleted(row, tx)) rethrow;
result = await _delegate.updateRow<T>(databaseRow, transaction: tx);
await _recorder.afterReinsert([result], tx);
_stripStampedRows([result], prepared);
return result;
}
await _recorder.afterInsert([result], tx);
_stripStampedRows([result], prepared);
return result;
},
);
return (await insert<T>([row], transaction: transaction)).single;
}
@override
@ -473,19 +471,23 @@ class CrdtDatabase implements Database {
Future<List<T>> _reinsertTombstonedRows<T extends TableRow>(
List<T> preparedRows,
List<T> result,
Transaction transaction,
) async {
Transaction transaction, {
Set<UuidValue>? deletedRowIds,
}) async {
final returnedIds = <Object>{
for (final row in result)
if (row.id != null) row.id as Object,
};
final rowsToReinsert = <T>[];
for (final row in preparedRows) {
final rowId = row.id;
if (rowId == null || returnedIds.contains(rowId)) continue;
if (await _recorder.isDeleted(row, transaction)) rowsToReinsert.add(row);
}
final missingRows = [
for (final row in preparedRows)
if (row.id case final rowId? when !returnedIds.contains(rowId)) row,
];
deletedRowIds ??= await _recorder.deletedRowIds(missingRows, transaction);
final rowsToReinsert = [
for (final row in missingRows)
if (deletedRowIds.contains(row.id)) row,
];
if (rowsToReinsert.isEmpty) return [];
final reinsertedRows = await _delegate.update<T>(
@ -496,6 +498,26 @@ class CrdtDatabase implements Database {
return reinsertedRows;
}
List<T> _orderAffectedRows<T extends TableRow>(
List<T> preparedRows,
List<T> insertedRows,
List<T> reinsertedRows,
) {
if (insertedRows.length + reinsertedRows.length != preparedRows.length) {
return [...insertedRows, ...reinsertedRows];
}
final reinsertedIds = {for (final row in reinsertedRows) row.id};
final inserted = insertedRows.iterator;
final reinserted = reinsertedRows.iterator;
return [
for (final row in preparedRows)
reinsertedIds.contains(row.id)
? (reinserted..moveNext()).current
: (inserted..moveNext()).current,
];
}
Future<List<T>> _rowsWithoutCrdtMetadata<T extends TableRow>(
List<T> rows,
Transaction transaction,

View file

@ -150,6 +150,13 @@ class CrdtDatabaseContext {
final result = <String, List<_ReferencingForeignKey>>{};
for (final table in _tableDefinitionsByName.values) {
final childTableName = table.name;
// Untracked children have no CRDT schema rows and cannot be
// soft-deleted; their physical rows keep referencing the (physically
// preserved) soft-deleted parent, so no delete action applies.
// Interim behavior: docs/sync-non-sync-relations.md forbids
// non-synced -> synced relations outright, but its initialize()
// validation is not implemented yet.
if (!_isCrdtTrackedTableName(childTableName)) continue;
for (final foreignKey in table.foreignKeys) {
if (foreignKey.columns.length != 1 ||
foreignKey.referenceColumns.length != 1) {
@ -973,22 +980,29 @@ WHERE c."id" IN ($whereRowIds)
);
}
/// Whether the given row is soft-deleted.
Future<bool> isDeleted<T extends TableRow>(
T row,
/// IDs of the given rows that are soft-deleted.
Future<Set<UuidValue>> deletedRowIds<T extends TableRow>(
List<T> rows,
Transaction transaction,
) async {
if (!isCrdtTracked<T>(row.table)) return false;
if (row.id is! UuidValue) return false;
final rowIds = {
for (final row in rows)
if (row.id is UuidValue) row.id as UuidValue,
};
if (rowIds.isEmpty) return {};
final table = rows.first.table;
if (!isCrdtTracked<T>(table)) return {};
final crdtRows = await _findCrdtRows(
row.table.tableName,
{row.id as UuidValue},
table.tableName,
rowIds,
transaction,
);
if (crdtRows.isEmpty) return false;
return crdtRows.single.isHidden;
return {
for (final row in crdtRows)
if (row.isHidden) row.uuidRowId,
};
}
Future<List<CrdtDataRow>> _findCrdtRows(