Test streams, updates and deletes

This commit is contained in:
Simon Binder 2019-02-14 17:25:41 +01:00
parent b7e37857ec
commit e4f733119d
10 changed files with 134 additions and 12 deletions

View File

@ -19,8 +19,9 @@ abstract class GeneratedDatabase {
List<TableInfo> get allTables;
GeneratedDatabase(this.typeSystem, this.executor,
{this.streamQueries = const StreamQueryStore()});
GeneratedDatabase(this.typeSystem, this.executor, {this.streamQueries}) {
streamQueries ??= StreamQueryStore();
}
/// Creates a migrator with the provided query executor. We sometimes can't
/// use the regular [GeneratedDatabase.executor] because migration happens

View File

@ -3,9 +3,9 @@ import 'dart:async';
import 'package:sally/sally.dart';
class StreamQueryStore {
final List<_QueryStream> _activeStreams = const [];
final List<_QueryStream> _activeStreams = [];
const StreamQueryStore();
StreamQueryStore();
Stream<List<T>> registerStream<T>(SelectStatement<dynamic, T> statement) {
final stream = _QueryStream(statement, this);

View File

@ -11,7 +11,9 @@ class InsertStatement<DataClass> {
InsertStatement(this.database, this.table);
Future<void> insert(DataClass entity) async {
table.validateIntegrity(entity, true);
if (!table.validateIntegrity(entity, true)) {
throw InvalidDataException('Invalid data: $entity cannot be written into ${table.$tableName}');
}
final map = table.entityToSql(entity)
..removeWhere((_, value) => value == null);

View File

@ -38,7 +38,9 @@ class UpdateStatement<T, D> extends Query<T, D> {
/// explicitly, this method will update all rows in the specific table.
Future<int> write(D entity) async {
_updateReference = entity;
table.validateIntegrity(_updateReference, false);
if (!table.validateIntegrity(_updateReference, false)) {
throw InvalidDataException('Invalid data: $entity cannot be written into ${table.$tableName}');
}
final ctx = constructQuery();
final rows = await ctx.database.executor.runUpdate(ctx.sql, ctx.boundVariables);

View File

@ -17,7 +17,7 @@ abstract class TableInfo<TableDsl, DataClass> {
/// that it respects all constraints (nullability, text length, etc.).
/// During insertion mode, fields that have a default value or are
/// auto-incrementing are allowed to be null as they will be set by sqlite.
void validateIntegrity(DataClass instance, bool isInserting) => null;
bool validateIntegrity(DataClass instance, bool isInserting) => null;
/// Maps the given data class into a map that can be inserted into sql. The
/// keys should represent the column name in sql, the values the corresponding

View File

@ -43,7 +43,7 @@ void main() {
verify(streamQueries.handleTableUpdates('users'));
});
test('are not issues when no data was changed', () async {
test('are not issued when no data was changed', () async {
when(executor.runDelete(any, any)).thenAnswer((_) => Future.value(0));
await db.delete(db.users).go();

View File

@ -0,0 +1,52 @@
import 'package:test_api/test_api.dart';
import 'tables/todos.dart';
import 'utils/mocks.dart';
void main() {
TodoDb db;
MockExecutor executor;
setUp(() {
executor = MockExecutor();
db = TodoDb(executor);
});
test('streams fetch when the first listener attaches', () {
final stream = db.select(db.users).watch();
verifyNever(executor.runSelect(any, any));
stream.listen((_) {});
verify(executor.runSelect(any, any)).called(1);
});
test('streams fetch when the underlying data changes', () {
db.select(db.users).watch().listen((_) {});
db.markTableUpdated('users');
// twice: Once because the listener attached, once because the data changed
verify(executor.runSelect(any, any)).called(2);
});
group("streams don't fetch", () {
test('when no listeners were attached', () {
db.select(db.users).watch();
db.markTableUpdated('users');
verifyNever(executor.runSelect(any, any));
});
test('when the data updates after the listener has detached', () {
final subscription = db.select(db.users).watch().listen((_) {});
clearInteractions(executor);
subscription.cancel();
db.markTableUpdated('users');
verifyNever(executor.runSelect(any, any));
});
});
}

View File

@ -55,7 +55,7 @@ class _$TodosTableTable extends TodosTable
@override
String get $tableName => 'todos';
@override
void validateIntegrity(TodoEntry instance, bool isInserting) =>
bool validateIntegrity(TodoEntry instance, bool isInserting) =>
id.isAcceptableValue(instance.id, isInserting) &&
title.isAcceptableValue(instance.title, isInserting) &&
content.isAcceptableValue(instance.content, isInserting) &&
@ -124,7 +124,7 @@ class _$CategoriesTable extends Categories
@override
String get $tableName => 'categories';
@override
void validateIntegrity(Category instance, bool isInserting) =>
bool validateIntegrity(Category instance, bool isInserting) =>
id.isAcceptableValue(instance.id, isInserting) &&
description.isAcceptableValue(instance.description, isInserting);
@override
@ -192,7 +192,7 @@ class _$UsersTable extends Users implements TableInfo<Users, User> {
@override
String get $tableName => 'users';
@override
void validateIntegrity(User instance, bool isInserting) =>
bool validateIntegrity(User instance, bool isInserting) =>
id.isAcceptableValue(instance.id, isInserting) &&
name.isAcceptableValue(instance.name, isInserting) &&
isAwesome.isAcceptableValue(instance.isAwesome, isInserting);

View File

@ -0,0 +1,65 @@
import 'package:sally/sally.dart';
import 'package:test_api/test_api.dart';
import 'tables/todos.dart';
import 'utils/mocks.dart';
void main() {
TodoDb db;
MockExecutor executor;
MockStreamQueries streamQueries;
setUp(() {
executor = MockExecutor();
streamQueries = MockStreamQueries();
db = TodoDb(executor)..streamQueries = streamQueries;
});
group('generates update statements', () {
test('for entire table', () async {
await db
.update(db.todosTable)
.write(TodoEntry(title: 'Updated title', category: 3));
verify(executor.runUpdate(
'UPDATE todos SET title = ? category = ?;', ['Updated title', 3]));
});
test('with WHERE and LIMIT clauses', () async {
await (db.update(db.todosTable)
..where((t) => t.id.isSmallerThan(50))
..limit(10))
.write(TodoEntry(title: 'Changed title'));
verify(executor.runUpdate(
'UPDATE todos SET title = ? WHERE id < ? LIMIT 10;',
['Changed title', 50]));
});
});
test('does not update with invalid data', () {
// The length of a title must be between 4 and 16 chars
expect(() async {
await db.into(db.todosTable).insert(TodoEntry(title: 'lol'));
}, throwsA(const TypeMatcher<InvalidDataException>()));
});
group('Table updates for delete statements', () {
test('are issued when data was changed', () async {
when(executor.runUpdate(any, any)).thenAnswer((_) => Future.value(3));
await db.update(db.todosTable).write(TodoEntry());
verify(streamQueries.handleTableUpdates('todos'));
});
test('are not issued when no data was changed', () async {
when(executor.runDelete(any, any)).thenAnswer((_) => Future.value(0));
await db.update(db.todosTable).write(TodoEntry());
verifyNever(streamQueries.handleTableUpdates(any));
});
});
}

View File

@ -146,7 +146,7 @@ class TableWriter {
void _writeValidityCheckMethod(StringBuffer buffer) {
final dataClass = table.dartTypeName;
buffer.write('@override\nvoid validateIntegrity($dataClass instance, bool isInserting) => ');
buffer.write('@override\nbool validateIntegrity($dataClass instance, bool isInserting) => ');
final validationCode = table.columns.map((column) {
final getterName = column.dartGetterName;