mirror of https://github.com/AMT-Cheif/drift.git
Auto-updating streams for queries
This commit is contained in:
parent
5909b0d3a2
commit
624d0980e0
|
@ -103,7 +103,6 @@ create an issue.
|
||||||
- Custom primary keys
|
- Custom primary keys
|
||||||
- Stabilize all end-user APIs and document them extensively
|
- Stabilize all end-user APIs and document them extensively
|
||||||
- Support default values and expressions, auto-increment
|
- Support default values and expressions, auto-increment
|
||||||
- Auto-updating streams for select statements
|
|
||||||
##### Definitely planned for the future
|
##### Definitely planned for the future
|
||||||
- Allow using DAOs instead of having to put everything in the main database
|
- Allow using DAOs instead of having to put everything in the main database
|
||||||
class.
|
class.
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import 'package:meta/meta.dart';
|
import 'package:meta/meta.dart';
|
||||||
import 'package:sally/sally.dart';
|
import 'package:sally/sally.dart';
|
||||||
|
import 'package:sally/src/runtime/executor/stream_queries.dart';
|
||||||
import 'package:sally/src/runtime/executor/type_system.dart';
|
import 'package:sally/src/runtime/executor/type_system.dart';
|
||||||
import 'package:sally/src/runtime/migration.dart';
|
import 'package:sally/src/runtime/migration.dart';
|
||||||
import 'package:sally/src/runtime/statements/delete.dart';
|
import 'package:sally/src/runtime/statements/delete.dart';
|
||||||
|
@ -10,9 +11,9 @@ import 'package:sally/src/runtime/statements/update.dart';
|
||||||
abstract class GeneratedDatabase {
|
abstract class GeneratedDatabase {
|
||||||
final SqlTypeSystem typeSystem;
|
final SqlTypeSystem typeSystem;
|
||||||
final QueryExecutor executor;
|
final QueryExecutor executor;
|
||||||
|
final StreamQueryStore streamQueries = StreamQueryStore();
|
||||||
|
|
||||||
int get schemaVersion;
|
int get schemaVersion;
|
||||||
|
|
||||||
MigrationStrategy get migration;
|
MigrationStrategy get migration;
|
||||||
|
|
||||||
List<TableInfo> get allTables;
|
List<TableInfo> get allTables;
|
||||||
|
@ -24,6 +25,10 @@ abstract class GeneratedDatabase {
|
||||||
/// before that executor is ready.
|
/// before that executor is ready.
|
||||||
Migrator _createMigrator(SqlExecutor executor) => Migrator(this, executor);
|
Migrator _createMigrator(SqlExecutor executor) => Migrator(this, executor);
|
||||||
|
|
||||||
|
void markTableUpdated(String tableName) {
|
||||||
|
streamQueries.handleTableUpdates(tableName);
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> handleDatabaseCreation({@required SqlExecutor executor}) {
|
Future<void> handleDatabaseCreation({@required SqlExecutor executor}) {
|
||||||
final migrator = _createMigrator(executor);
|
final migrator = _createMigrator(executor);
|
||||||
return migration.onCreate(migrator);
|
return migration.onCreate(migrator);
|
||||||
|
|
|
@ -0,0 +1,71 @@
|
||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:sally/sally.dart';
|
||||||
|
|
||||||
|
class StreamQueryStore {
|
||||||
|
final List<_QueryStream> _activeStreams = [];
|
||||||
|
|
||||||
|
Stream<List<T>> registerStream<T>(SelectStatement<dynamic, T> statement) {
|
||||||
|
final stream = _QueryStream(statement, this);
|
||||||
|
_activeStreams.add(stream);
|
||||||
|
return stream.stream;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> handleTableUpdates(String table) async {
|
||||||
|
final affectedStreams = _activeStreams.where((stream) => stream.isAffectedByTableChange(table));
|
||||||
|
|
||||||
|
for (var stream in affectedStreams) {
|
||||||
|
await stream.fetchAndEmitData();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _markAsClosed(_QueryStream stream) {
|
||||||
|
_activeStreams.remove(stream);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _QueryStream<T, D> {
|
||||||
|
final SelectStatement<T, D> query;
|
||||||
|
final StreamQueryStore _store;
|
||||||
|
|
||||||
|
StreamController<List<D>> _controller;
|
||||||
|
|
||||||
|
Stream<List<D>> get stream {
|
||||||
|
_controller ??= StreamController.broadcast(
|
||||||
|
onListen: _onListen,
|
||||||
|
onCancel: _onCancel,
|
||||||
|
);
|
||||||
|
|
||||||
|
return _controller.stream;
|
||||||
|
}
|
||||||
|
|
||||||
|
_QueryStream(this.query, this._store);
|
||||||
|
|
||||||
|
void _onListen() {
|
||||||
|
// first listener added, fetch query
|
||||||
|
fetchAndEmitData();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onCancel() {
|
||||||
|
// last listener gone, dispose
|
||||||
|
_controller.close();
|
||||||
|
// todo this removes the stream from the list so that it can be garbage
|
||||||
|
// collected. When a stream is never listened to, we have a memory leak as
|
||||||
|
// this will never be called. Maybe an Expando would help here?
|
||||||
|
_store._markAsClosed(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> fetchAndEmitData() async {
|
||||||
|
if (!_controller.hasListener) return;
|
||||||
|
|
||||||
|
final data = await query.get();
|
||||||
|
|
||||||
|
if (!_controller.isClosed) {
|
||||||
|
_controller.add(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isAffectedByTableChange(String table) {
|
||||||
|
return table == query.table.$tableName;
|
||||||
|
}
|
||||||
|
}
|
|
@ -16,6 +16,12 @@ class DeleteStatement<UserTable> extends Query<UserTable, dynamic> {
|
||||||
Future<int> go() async {
|
Future<int> go() async {
|
||||||
final ctx = constructQuery();
|
final ctx = constructQuery();
|
||||||
|
|
||||||
return await ctx.database.executor.runDelete(ctx.sql, ctx.boundVariables);
|
final rows = await ctx.database.executor.runDelete(ctx.sql, ctx.boundVariables);
|
||||||
|
|
||||||
|
if (rows > 0) {
|
||||||
|
database.markTableUpdated(table.$tableName);
|
||||||
|
}
|
||||||
|
|
||||||
|
return rows;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -37,7 +37,8 @@ class InsertStatement<DataClass> {
|
||||||
|
|
||||||
ctx.buffer.write(')');
|
ctx.buffer.write(')');
|
||||||
|
|
||||||
return database.executor.runInsert(ctx.sql, ctx.boundVariables);
|
await database.executor.runInsert(ctx.sql, ctx.boundVariables);
|
||||||
|
database.markTableUpdated(table.$tableName);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO insert multiple values
|
// TODO insert multiple values
|
||||||
|
|
|
@ -13,7 +13,6 @@ import 'package:sally/src/runtime/structure/table_info.dart';
|
||||||
abstract class Query<Table, DataClass> {
|
abstract class Query<Table, DataClass> {
|
||||||
@protected
|
@protected
|
||||||
GeneratedDatabase database;
|
GeneratedDatabase database;
|
||||||
@protected
|
|
||||||
TableInfo<Table, DataClass> table;
|
TableInfo<Table, DataClass> table;
|
||||||
|
|
||||||
Query(this.database, this.table);
|
Query(this.database, this.table);
|
||||||
|
|
|
@ -20,4 +20,10 @@ class SelectStatement<T, D> extends Query<T, D> {
|
||||||
await ctx.database.executor.runSelect(ctx.sql, ctx.boundVariables);
|
await ctx.database.executor.runSelect(ctx.sql, ctx.boundVariables);
|
||||||
return results.map(table.map).toList();
|
return results.map(table.map).toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Creates an auto-updating stream that emits new items whenever this table
|
||||||
|
/// changes.
|
||||||
|
Stream<List<D>> watch() {
|
||||||
|
return database.streamQueries.registerStream(this);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -36,11 +36,17 @@ class UpdateStatement<T, D> extends Query<T, D> {
|
||||||
/// that match the set [where] and [limit] constraints. Warning: That also
|
/// that match the set [where] and [limit] constraints. Warning: That also
|
||||||
/// means that, when you're not setting a where or limit expression
|
/// means that, when you're not setting a where or limit expression
|
||||||
/// 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) {
|
Future<int> write(D entity) async {
|
||||||
_updateReference = entity;
|
_updateReference = entity;
|
||||||
table.validateIntegrity(_updateReference, false);
|
table.validateIntegrity(_updateReference, false);
|
||||||
|
|
||||||
final ctx = constructQuery();
|
final ctx = constructQuery();
|
||||||
return ctx.database.executor.runUpdate(ctx.sql, ctx.boundVariables);
|
final rows = await ctx.database.executor.runUpdate(ctx.sql, ctx.boundVariables);
|
||||||
|
|
||||||
|
if (rows > 0) {
|
||||||
|
database.markTableUpdated(table.$tableName);
|
||||||
|
}
|
||||||
|
|
||||||
|
return rows;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,6 +15,9 @@ void main() {
|
||||||
db = TestDatabase(executor);
|
db = TestDatabase(executor);
|
||||||
|
|
||||||
when(executor.runSelect(any, any)).thenAnswer((_) => Future.value([]));
|
when(executor.runSelect(any, any)).thenAnswer((_) => Future.value([]));
|
||||||
|
when(executor.runUpdate(any, any)).thenAnswer((_) => Future.value(0));
|
||||||
|
when(executor.runDelete(any, any)).thenAnswer((_) => Future.value(0));
|
||||||
|
when(executor.runInsert(any, any)).thenAnswer((_) => Future.value(0));
|
||||||
});
|
});
|
||||||
|
|
||||||
group('Generates SELECT statements', () {
|
group('Generates SELECT statements', () {
|
||||||
|
@ -54,6 +57,17 @@ void main() {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
group('Streams for queries', () {
|
||||||
|
test('update correctly', () {
|
||||||
|
final stream = db.select(db.users).watch();
|
||||||
|
stream.listen((_) => null);
|
||||||
|
|
||||||
|
db.markTableUpdated('users');
|
||||||
|
|
||||||
|
verify(executor.runSelect('SELECT * FROM users;', argThat(isEmpty))).called(2);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
group('Generates DELETE statements', () {
|
group('Generates DELETE statements', () {
|
||||||
test('without any constraints', () {
|
test('without any constraints', () {
|
||||||
db.delete(db.users).go();
|
db.delete(db.users).go();
|
||||||
|
|
Loading…
Reference in New Issue