Generate more accurate update rules for triggers

This commit is contained in:
Simon Binder 2020-03-04 20:59:03 +01:00
parent 0b0d5792fd
commit b0b9a0ed47
No known key found for this signature in database
GPG Key ID: 7891917E4147B8C0
11 changed files with 87 additions and 40 deletions

View File

@ -17,6 +17,8 @@
- Experimentally support IndexedDB to store sqlite data on the web
- Moor will no longer wait for query stream listeners to receive a done event when closing a database
or transaction.
- Updated stream queries: They now take triggers into account and more accurately detect when an update
is necessary.
## 2.4.1

View File

@ -57,10 +57,11 @@ class WritePropagation extends UpdateRule {
final TableUpdateQuery on;
/// All updates that will be performed by the trigger listening on [on].
final Set<TableUpdate> result;
final List<TableUpdate> result;
/// Default constructor. See [WritePropagation] for details.
const WritePropagation(this.on, this.result) : super._();
const WritePropagation({@required this.on, @required this.result})
: super._();
}
/// Classifies a [TableUpdate] by what kind of write happened - an insert, an
@ -90,6 +91,12 @@ class TableUpdate {
/// Default constant constructor.
const TableUpdate(this.table, {this.kind});
/// Creates a [TableUpdate] instance based on a [TableInfo] instead of the raw
/// name.
factory TableUpdate.fromTable(TableInfo table, {UpdateKind kind}) {
return TableUpdate(table.actualTableName, kind: kind);
}
@override
int get hashCode => $mrjf($mrjc(kind.hashCode, table.hashCode));
@ -97,6 +104,11 @@ class TableUpdate {
bool operator ==(dynamic other) {
return other is TableUpdate && other.kind == kind && other.table == table;
}
@override
String toString() {
return 'TableUpdate($table, kind: $kind)';
}
}
/// A table update query describes information to listen for [TableUpdate]s.
@ -111,7 +123,7 @@ abstract class TableUpdateQuery {
const factory TableUpdateQuery.any() = AnyUpdateQuery;
/// A query that listens for all updates that match any query in [queries].
const factory TableUpdateQuery.allOf(Set<TableUpdateQuery> queries) =
const factory TableUpdateQuery.allOf(List<TableUpdateQuery> queries) =
MultipleUpdateQuery;
/// A query that listens for all updates on a specific [table].

View File

@ -275,7 +275,7 @@ class AnyUpdateQuery extends TableUpdateQuery {
}
class MultipleUpdateQuery extends TableUpdateQuery {
final Set<TableUpdateQuery> queries;
final List<TableUpdateQuery> queries;
const MultipleUpdateQuery(this.queries);
@ -297,4 +297,14 @@ class SpecificUpdateQuery extends TableUpdateQuery {
limitUpdateKind == null ||
update.kind == limitUpdateKind;
}
@override
int get hashCode => $mrjf($mrjc(limitUpdateKind.hashCode, table.hashCode));
@override
bool operator ==(dynamic other) {
return other is SpecificUpdateQuery &&
other.limitUpdateKind == limitUpdateKind &&
other.table == table;
}
}

View File

@ -32,7 +32,8 @@ class DeleteStatement<T extends Table, D extends DataClass> extends Query<T, D>
final rows = await ctx.executor.runDelete(ctx.sql, ctx.boundVariables);
if (rows > 0) {
database.markTablesUpdated({table});
database.notifyUpdates(
{TableUpdate.fromTable(table, kind: UpdateKind.delete)});
}
return rows;
});

View File

@ -36,7 +36,8 @@ class InsertStatement<D extends DataClass> {
return await database.executor.doWhenOpened((e) async {
final id = await database.executor.runInsert(ctx.sql, ctx.boundVariables);
database.markTablesUpdated({table});
database.notifyUpdates(
{TableUpdate.fromTable(table, kind: UpdateKind.insert)});
return id;
});
}

View File

@ -1273,11 +1273,17 @@ abstract class _$CustomTablesDb extends GeneratedDatabase {
email
];
@override
StreamQueryUpdateRules get streamUpdateRules => const StreamQueryUpdateRules([
WritePropagation(
TableUpdateQuery.onTable('config', limitUpdateKind: null),
{TableUpdate('with_defaults', kind: null)})
]);
StreamQueryUpdateRules get streamUpdateRules => const StreamQueryUpdateRules(
[
WritePropagation(
on: TableUpdateQuery.onTable('config',
limitUpdateKind: UpdateKind.insert),
result: [
TableUpdate('with_defaults', kind: null),
],
),
],
);
}
class TableValuedResult {

View File

@ -230,13 +230,24 @@ void main() {
});
});
// note: There's a trigger on config inserts that updates with_defaults
test('updates streams for updates caused by triggers', () async {
final db = CustomTablesDb(executor);
db.select(db.withDefaults).watch().listen((_) {});
db.markTablesUpdated({db.config});
db.notifyUpdates({const TableUpdate('config', kind: UpdateKind.insert)});
await pumpEventQueue(times: 1);
verify(executor.runSelect(any, any)).called(2);
});
test('limits trigger propagation to the target type of trigger', () async {
final db = CustomTablesDb(executor);
db.select(db.withDefaults).watch().listen((_) {});
db.notifyUpdates({const TableUpdate('config', kind: UpdateKind.delete)});
await pumpEventQueue(times: 1);
verify(executor.runSelect(any, any)).called(1);
});
}

View File

@ -8,7 +8,7 @@ class MoorTrigger implements MoorSchemaEntity {
final String displayName;
@override
final TriggerDeclaration declaration;
final MoorTriggerDeclaration declaration;
/// The table on which this trigger operates.
///

View File

@ -1,5 +1,6 @@
import 'package:moor/moor.dart';
import 'package:moor_generator/moor_generator.dart';
import 'package:sqlparser/sqlparser.dart';
class FindStreamUpdateRules {
final Database db;
@ -10,13 +11,26 @@ class FindStreamUpdateRules {
final rules = <UpdateRule>[];
for (final trigger in db.entities.whereType<MoorTrigger>()) {
final target = trigger.declaration.node.target;
UpdateKind targetKind;
if (target is DeleteTarget) {
targetKind = UpdateKind.delete;
} else if (target is InsertTarget) {
targetKind = UpdateKind.insert;
} else {
targetKind = UpdateKind.update;
}
rules.add(
WritePropagation(
TableUpdateQuery.onTable(trigger.on.sqlName),
{
on: TableUpdateQuery.onTable(
trigger.on.sqlName,
limitUpdateKind: targetKind,
),
result: [
for (final update in trigger.bodyUpdates)
TableUpdate(update.sqlName)
},
],
),
);
}

View File

@ -116,16 +116,12 @@ class DatabaseWriter {
..write('@override\nStreamQueryUpdateRules get streamUpdateRules => ')
..write('const StreamQueryUpdateRules([');
var isFirst = true;
for (final rule in updateRules.rules) {
if (!isFirst) {
schemaScope.write(', ');
}
isFirst = false;
rule.writeConstructor(schemaScope);
schemaScope.write(', ');
}
schemaScope.write(']);\n');
schemaScope.write('],);\n');
}
// close the class
@ -145,21 +141,16 @@ extension on UpdateRule {
if (this is WritePropagation) {
final write = this as WritePropagation;
buffer.write('WritePropagation(');
buffer.write('WritePropagation(on: ');
write.on.writeConstructor(buffer);
buffer.write(', {');
buffer.write(', result: [');
var isFirst = true;
for (final update in write.result) {
if (!isFirst) {
buffer.write(', ');
}
isFirst = false;
update.writeConstructor(buffer);
buffer.write(', ');
}
buffer.write('})');
buffer.write('],)');
}
}
}
@ -181,18 +172,13 @@ extension on TableUpdateQuery {
'limitUpdateKind: ${_kindToDartExpr[query.limitUpdateKind]})');
} else if (this is MultipleUpdateQuery) {
final queries = (this as MultipleUpdateQuery).queries;
var isFirst = true;
buffer.write('TableUpdateQuery.allOf({');
buffer.write('TableUpdateQuery.allOf([');
for (final query in queries) {
if (!isFirst) {
buffer.write(', ');
}
isFirst = false;
query.writeConstructor(buffer);
buffer.write(', ');
}
buffer.write('})');
buffer.write('])');
}
}
}

View File

@ -39,7 +39,11 @@ class MyDatabase {}
expect(
rules.rules.single,
isA<WritePropagation>()
.having((e) => e.on, 'on', const TableUpdateQuery.onTable('users'))
.having(
(e) => e.on,
'on',
const TableUpdateQuery.onTable('users',
limitUpdateKind: UpdateKind.insert))
.having((e) => e.result, 'result', {const TableUpdate('users')}),
);
});