mirror of https://github.com/AMT-Cheif/drift.git
Test streams, updates and deletes
This commit is contained in:
parent
b7e37857ec
commit
e4f733119d
|
@ -19,8 +19,9 @@ abstract class GeneratedDatabase {
|
||||||
|
|
||||||
List<TableInfo> get allTables;
|
List<TableInfo> get allTables;
|
||||||
|
|
||||||
GeneratedDatabase(this.typeSystem, this.executor,
|
GeneratedDatabase(this.typeSystem, this.executor, {this.streamQueries}) {
|
||||||
{this.streamQueries = const StreamQueryStore()});
|
streamQueries ??= StreamQueryStore();
|
||||||
|
}
|
||||||
|
|
||||||
/// Creates a migrator with the provided query executor. We sometimes can't
|
/// Creates a migrator with the provided query executor. We sometimes can't
|
||||||
/// use the regular [GeneratedDatabase.executor] because migration happens
|
/// use the regular [GeneratedDatabase.executor] because migration happens
|
||||||
|
|
|
@ -3,9 +3,9 @@ import 'dart:async';
|
||||||
import 'package:sally/sally.dart';
|
import 'package:sally/sally.dart';
|
||||||
|
|
||||||
class StreamQueryStore {
|
class StreamQueryStore {
|
||||||
final List<_QueryStream> _activeStreams = const [];
|
final List<_QueryStream> _activeStreams = [];
|
||||||
|
|
||||||
const StreamQueryStore();
|
StreamQueryStore();
|
||||||
|
|
||||||
Stream<List<T>> registerStream<T>(SelectStatement<dynamic, T> statement) {
|
Stream<List<T>> registerStream<T>(SelectStatement<dynamic, T> statement) {
|
||||||
final stream = _QueryStream(statement, this);
|
final stream = _QueryStream(statement, this);
|
||||||
|
|
|
@ -11,7 +11,9 @@ class InsertStatement<DataClass> {
|
||||||
InsertStatement(this.database, this.table);
|
InsertStatement(this.database, this.table);
|
||||||
|
|
||||||
Future<void> insert(DataClass entity) async {
|
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)
|
final map = table.entityToSql(entity)
|
||||||
..removeWhere((_, value) => value == null);
|
..removeWhere((_, value) => value == null);
|
||||||
|
|
|
@ -38,7 +38,9 @@ class UpdateStatement<T, D> extends Query<T, D> {
|
||||||
/// explicitly, this method will update all rows in the specific table.
|
/// explicitly, this method will update all rows in the specific table.
|
||||||
Future<int> write(D entity) async {
|
Future<int> write(D entity) async {
|
||||||
_updateReference = entity;
|
_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 ctx = constructQuery();
|
||||||
final rows = await ctx.database.executor.runUpdate(ctx.sql, ctx.boundVariables);
|
final rows = await ctx.database.executor.runUpdate(ctx.sql, ctx.boundVariables);
|
||||||
|
|
|
@ -17,7 +17,7 @@ abstract class TableInfo<TableDsl, DataClass> {
|
||||||
/// that it respects all constraints (nullability, text length, etc.).
|
/// that it respects all constraints (nullability, text length, etc.).
|
||||||
/// During insertion mode, fields that have a default value or are
|
/// 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.
|
/// 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
|
/// 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
|
/// keys should represent the column name in sql, the values the corresponding
|
||||||
|
|
|
@ -43,7 +43,7 @@ void main() {
|
||||||
verify(streamQueries.handleTableUpdates('users'));
|
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));
|
when(executor.runDelete(any, any)).thenAnswer((_) => Future.value(0));
|
||||||
|
|
||||||
await db.delete(db.users).go();
|
await db.delete(db.users).go();
|
||||||
|
|
|
@ -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));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
|
@ -55,7 +55,7 @@ class _$TodosTableTable extends TodosTable
|
||||||
@override
|
@override
|
||||||
String get $tableName => 'todos';
|
String get $tableName => 'todos';
|
||||||
@override
|
@override
|
||||||
void validateIntegrity(TodoEntry instance, bool isInserting) =>
|
bool validateIntegrity(TodoEntry instance, bool isInserting) =>
|
||||||
id.isAcceptableValue(instance.id, isInserting) &&
|
id.isAcceptableValue(instance.id, isInserting) &&
|
||||||
title.isAcceptableValue(instance.title, isInserting) &&
|
title.isAcceptableValue(instance.title, isInserting) &&
|
||||||
content.isAcceptableValue(instance.content, isInserting) &&
|
content.isAcceptableValue(instance.content, isInserting) &&
|
||||||
|
@ -124,7 +124,7 @@ class _$CategoriesTable extends Categories
|
||||||
@override
|
@override
|
||||||
String get $tableName => 'categories';
|
String get $tableName => 'categories';
|
||||||
@override
|
@override
|
||||||
void validateIntegrity(Category instance, bool isInserting) =>
|
bool validateIntegrity(Category instance, bool isInserting) =>
|
||||||
id.isAcceptableValue(instance.id, isInserting) &&
|
id.isAcceptableValue(instance.id, isInserting) &&
|
||||||
description.isAcceptableValue(instance.description, isInserting);
|
description.isAcceptableValue(instance.description, isInserting);
|
||||||
@override
|
@override
|
||||||
|
@ -192,7 +192,7 @@ class _$UsersTable extends Users implements TableInfo<Users, User> {
|
||||||
@override
|
@override
|
||||||
String get $tableName => 'users';
|
String get $tableName => 'users';
|
||||||
@override
|
@override
|
||||||
void validateIntegrity(User instance, bool isInserting) =>
|
bool validateIntegrity(User instance, bool isInserting) =>
|
||||||
id.isAcceptableValue(instance.id, isInserting) &&
|
id.isAcceptableValue(instance.id, isInserting) &&
|
||||||
name.isAcceptableValue(instance.name, isInserting) &&
|
name.isAcceptableValue(instance.name, isInserting) &&
|
||||||
isAwesome.isAcceptableValue(instance.isAwesome, isInserting);
|
isAwesome.isAcceptableValue(instance.isAwesome, isInserting);
|
||||||
|
|
|
@ -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));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
|
@ -146,7 +146,7 @@ class TableWriter {
|
||||||
void _writeValidityCheckMethod(StringBuffer buffer) {
|
void _writeValidityCheckMethod(StringBuffer buffer) {
|
||||||
final dataClass = table.dartTypeName;
|
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 validationCode = table.columns.map((column) {
|
||||||
final getterName = column.dartGetterName;
|
final getterName = column.dartGetterName;
|
||||||
|
|
Loading…
Reference in New Issue