diff --git a/moor_generator/lib/src/analyzer/moor/entity_handler.dart b/moor_generator/lib/src/analyzer/moor/entity_handler.dart index 2418ed73..3d1d80ef 100644 --- a/moor_generator/lib/src/analyzer/moor/entity_handler.dart +++ b/moor_generator/lib/src/analyzer/moor/entity_handler.dart @@ -14,13 +14,34 @@ class EntityHandler { EntityHandler(this.step, this.file, this.availableTables); + final Map _triggers = {}; + final Map _tables = {}; + void handle() { + final referenceResolver = _ReferenceResolvingVisitor(this); for (final table in file.declaredTables) { table.references.clear(); + + final declaration = table.declaration as MoorTableDeclaration; + _tables[declaration.node] = table; + declaration.node.acceptWithoutArg(referenceResolver); } - file.parseResult.rootNode - ?.acceptWithoutArg(_ReferenceResolvingVisitor(this)); + for (final trigger in file.declaredEntities.whereType()) { + trigger.on = null; + + final declaration = trigger.declaration as MoorTriggerDeclaration; + _triggers[declaration.node] = trigger; + declaration.node.acceptWithoutArg(referenceResolver); + } + } + + MoorTable _inducedTable(TableInducingStatement stmt) { + return _tables[stmt]; + } + + MoorTrigger _inducedTrigger(CreateTriggerStatement stmt) { + return _triggers[stmt]; } } @@ -29,22 +50,42 @@ class _ReferenceResolvingVisitor extends RecursiveVisitor { _ReferenceResolvingVisitor(this.handler); + MoorTable _resolveTable(TableReference reference) { + return handler.availableTables.singleWhere( + (t) => t.sqlName == reference.tableName, + orElse: () => null); + } + + @override + void visitCreateTriggerStatement(CreateTriggerStatement e, void arg) { + final table = _resolveTable(e.onTable); + if (table == null) { + handler.step.reportError(ErrorInMoorFile( + severity: Severity.error, + span: e.onTable.span, + message: 'Target table ${e.onTable.tableName} could not be found.', + )); + } else { + final moorTrigger = handler._inducedTrigger(e); + moorTrigger?.on = table; + } + } + @override void visitForeignKeyClause(ForeignKeyClause clause, void arg) { final stmt = clause.parents.whereType().first; - final referencedTable = handler.availableTables.singleWhere( - (t) => t.sqlName == clause.foreignTable.tableName, - orElse: () => null); + final referencedTable = _resolveTable(clause.foreignTable); if (referencedTable == null) { handler.step.reportError(ErrorInMoorFile( - severity: Severity.error, - span: clause.span, - message: - 'Referenced table ${clause.foreignTable.tableName} could not be' - 'found.')); + severity: Severity.error, + span: clause.span, + message: + 'Referenced table ${clause.foreignTable.tableName} could not be' + 'found.', + )); } else { - final createdTable = handler.file.tableDeclarations[stmt]; + final createdTable = handler._inducedTable(stmt); createdTable?.references?.add(referencedTable); } diff --git a/moor_generator/lib/src/analyzer/moor/parser.dart b/moor_generator/lib/src/analyzer/moor/parser.dart index 3077a801..c11ba11a 100644 --- a/moor_generator/lib/src/analyzer/moor/parser.dart +++ b/moor_generator/lib/src/analyzer/moor/parser.dart @@ -20,6 +20,8 @@ class MoorParser { final queryDeclarations = []; final importStatements = []; + final createdEntities = []; + for (final parsedStmt in parsedFile.statements) { if (parsedStmt is ImportStatement) { final importStmt = parsedStmt; @@ -27,6 +29,9 @@ class MoorParser { importStatements.add(importStmt); } else if (parsedStmt is TableInducingStatement) { createdReaders.add(CreateTableReader(parsedStmt, step)); + } else if (parsedStmt is CreateTriggerStatement) { + // the table will be resolved in the analysis step + createdEntities.add(MoorTrigger.fromMoor(parsedStmt, step.file)); } else if (parsedStmt is DeclaredStatement) { if (parsedStmt.isRegularQuery) { queryDeclarations.add(DeclaredMoorQuery.fromStatement(parsedStmt)); @@ -42,20 +47,18 @@ class MoorParser { )); } - final createdTables = []; final tableDeclarations = {}; for (final reader in createdReaders) { final table = reader.extractTable(step.mapper); - createdTables.add(table); + createdEntities.add(table); tableDeclarations[reader.stmt] = table; } final analyzedFile = ParsedMoorFile( result, - declaredTables: createdTables, + declaredEntities: createdEntities, queries: queryDeclarations, imports: importStatements, - tableDeclarations: tableDeclarations, ); for (final decl in queryDeclarations) { decl.file = analyzedFile; diff --git a/moor_generator/lib/src/analyzer/runner/results.dart b/moor_generator/lib/src/analyzer/runner/results.dart index 61c618b2..fbcc06fc 100644 --- a/moor_generator/lib/src/analyzer/runner/results.dart +++ b/moor_generator/lib/src/analyzer/runner/results.dart @@ -42,14 +42,13 @@ class ParsedMoorFile extends FileResult { final List otherComponents; List resolvedQueries; - Map tableDeclarations; Map resolvedImports; - ParsedMoorFile(this.parseResult, - {List declaredTables = const [], - this.queries = const [], - this.imports = const [], - this.otherComponents = const [], - this.tableDeclarations = const {}}) - : super(declaredTables); + ParsedMoorFile( + this.parseResult, { + List declaredEntities = const [], + this.queries = const [], + this.imports = const [], + this.otherComponents = const [], + }) : super(declaredEntities); } diff --git a/moor_generator/lib/src/model/base_entity.dart b/moor_generator/lib/src/model/base_entity.dart index ea6b7adb..11d34f3d 100644 --- a/moor_generator/lib/src/model/base_entity.dart +++ b/moor_generator/lib/src/model/base_entity.dart @@ -1,7 +1,9 @@ +import 'package:moor_generator/moor_generator.dart'; + /// Some schema entity found. /// /// Most commonly a table, but it can also be a trigger. -abstract class MoorSchemaEntity { +abstract class MoorSchemaEntity implements HasDeclaration { /// All entities that have to be created before this entity can be created. /// /// For tables, this can be contents of a `REFERENCES` clause. For triggers, diff --git a/moor_generator/lib/src/model/declarations/declaration.dart b/moor_generator/lib/src/model/declarations/declaration.dart index 161af03f..060b85bc 100644 --- a/moor_generator/lib/src/model/declarations/declaration.dart +++ b/moor_generator/lib/src/model/declarations/declaration.dart @@ -6,6 +6,7 @@ import 'package:sqlparser/sqlparser.dart'; part 'columns.dart'; part 'database.dart'; part 'tables.dart'; +part 'trigger.dart'; /// Interface for model elements that are declared somewhere. abstract class HasDeclaration { diff --git a/moor_generator/lib/src/model/declarations/trigger.dart b/moor_generator/lib/src/model/declarations/trigger.dart new file mode 100644 index 00000000..9af45355 --- /dev/null +++ b/moor_generator/lib/src/model/declarations/trigger.dart @@ -0,0 +1,16 @@ +part of 'declaration.dart'; + +abstract class TriggerDeclaration extends Declaration {} + +class MoorTriggerDeclaration implements MoorDeclaration, TriggerDeclaration { + @override + final SourceRange declaration; + + @override + final CreateTriggerStatement node; + + MoorTriggerDeclaration._(this.declaration, this.node); + + MoorTriggerDeclaration.fromNodeAndFile(this.node, FoundFile file) + : declaration = SourceRange.fromNodeAndFile(node, file); +} diff --git a/moor_generator/lib/src/model/model.dart b/moor_generator/lib/src/model/model.dart index 3c29d7bf..07ae2be1 100644 --- a/moor_generator/lib/src/model/model.dart +++ b/moor_generator/lib/src/model/model.dart @@ -5,4 +5,5 @@ export 'declarations/declaration.dart'; export 'sources.dart'; export 'sql_query.dart'; export 'table.dart'; +export 'trigger.dart'; export 'used_type_converter.dart'; diff --git a/moor_generator/lib/src/model/table.dart b/moor_generator/lib/src/model/table.dart index 231c5fa1..57ce466b 100644 --- a/moor_generator/lib/src/model/table.dart +++ b/moor_generator/lib/src/model/table.dart @@ -10,7 +10,7 @@ import 'declarations/declaration.dart'; /// A parsed table, declared in code by extending `Table` and referencing that /// table in `@UseMoor` or `@UseDao`. -class MoorTable implements HasDeclaration, MoorSchemaEntity { +class MoorTable implements MoorSchemaEntity { /// The [ClassElement] for the class that declares this table or null if /// the table was inferred from a `CREATE TABLE` statement. final ClassElement fromClass; diff --git a/moor_generator/lib/src/model/trigger.dart b/moor_generator/lib/src/model/trigger.dart new file mode 100644 index 00000000..31479cd8 --- /dev/null +++ b/moor_generator/lib/src/model/trigger.dart @@ -0,0 +1,30 @@ +import 'package:moor_generator/src/analyzer/runner/file_graph.dart'; +import 'package:sqlparser/sqlparser.dart'; + +import 'model.dart'; + +class MoorTrigger implements MoorSchemaEntity { + @override + final String displayName; + + @override + final Declaration declaration; + + /// The table on which this trigger operates. + /// + /// This field can be null in case the table wasn't resolved. + MoorTable on; + + MoorTrigger(this.displayName, this.declaration, this.on); + + factory MoorTrigger.fromMoor(CreateTriggerStatement stmt, FoundFile file) { + return MoorTrigger( + stmt.triggerName, + MoorTriggerDeclaration.fromNodeAndFile(stmt, file), + null, // must be resolved later + ); + } + + @override + Iterable get references => [on]; +} diff --git a/moor_generator/test/analyzer/cyclic_moor_dart_dependency.dart b/moor_generator/test/analyzer/cyclic_moor_dart_dependency_test.dart similarity index 100% rename from moor_generator/test/analyzer/cyclic_moor_dart_dependency.dart rename to moor_generator/test/analyzer/cyclic_moor_dart_dependency_test.dart diff --git a/moor_generator/test/analyzer/moor/entity_handler_test.dart b/moor_generator/test/analyzer/moor/entity_handler_test.dart new file mode 100644 index 00000000..331020eb --- /dev/null +++ b/moor_generator/test/analyzer/moor/entity_handler_test.dart @@ -0,0 +1,106 @@ +import 'package:moor_generator/moor_generator.dart'; +import 'package:test/test.dart'; + +import '../utils.dart'; + +void main() { + group('finds referenced tables', () { + const definitions = { + 'foo|lib/b.moor': ''' +CREATE TABLE users ( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + name VARCHAR +); +''' + }; + + test('in a foreign key clause', () async { + final state = TestState.withContent(const { + 'foo|lib/a.moor': ''' +import 'b.moor'; + +CREATE TABLE friendships ( + user_a INTEGER REFERENCES users (id), + user_b INTEGER REFERENCES users (id), + + PRIMARY KEY(user_a, user_b), + CHECK (user_a != user_b) +); + ''', + ...definitions, + }); + + final file = await state.analyze('package:foo/a.moor'); + expect(file.errors.errors, isEmpty); + + final table = file.currentResult.declaredTables.single; + expect( + table.references, + [ + const TypeMatcher() + .having((table) => table.displayName, 'displayName', 'users') + ], + ); + }); + + test('in a trigger', () async { + final state = TestState.withContent(const { + 'foo|lib/a.moor': ''' +import 'b.moor'; + +CREATE TRIGGER my_trigger AFTER DELETE ON users BEGIN + SELECT * FROM users; +END; + ''', + ...definitions, + }); + + final file = await state.analyze('package:foo/a.moor'); + expect(file.errors.errors, isEmpty); + + final trigger = file.currentResult.declaredEntities.single; + expect( + trigger.references, + [ + const TypeMatcher() + .having((table) => table.displayName, 'displayName', 'users') + ], + ); + }); + }); + + group('issues error when referencing an unknown table', () { + test('in a foreign key clause', () async { + final state = TestState.withContent(const { + 'foo|lib/a.moor': ''' +CREATE TABLE foo ( + id INTEGER NOT NULL REFERENCES bar (baz) PRIMARY KEY +); + ''' + }); + + final file = await state.analyze('package:foo/a.moor'); + + expect( + file.errors.errors.map((e) => e.message), + contains('Referenced table bar could not befound.'), + ); + }); + + test('in a trigger', () async { + final state = TestState.withContent(const { + 'foo|lib/a.moor': ''' +CREATE TRIGGER IF NOT EXISTS foo BEFORE DELETE ON bar BEGIN +END; + ''', + }); + + final file = await state.analyze('package:foo/a.moor'); + + expect( + file.errors.errors.map((e) => e.message), + contains('Target table bar could not be found.'), + ); + }); + }); +} diff --git a/moor_generator/test/analyzer/utils.dart b/moor_generator/test/analyzer/utils.dart index 040d6dbc..0627201d 100644 --- a/moor_generator/test/analyzer/utils.dart +++ b/moor_generator/test/analyzer/utils.dart @@ -30,4 +30,9 @@ class TestState { FoundFile file(String uri) { return session.registerFile(Uri.parse(uri)); } + + Future analyze(String uri) async { + await runTask(uri); + return file(uri); + } } diff --git a/sqlparser/CHANGELOG.md b/sqlparser/CHANGELOG.md index 98702e07..05bef13d 100644 --- a/sqlparser/CHANGELOG.md +++ b/sqlparser/CHANGELOG.md @@ -3,6 +3,7 @@ - Added a argument type and argument to the visitor classes - Experimental new type inference algorithm - Support `CAST` expressions. +- Support parsing `CREATE TRIGGER` statements ## 0.5.0 - Optionally support the `json1` module diff --git a/sqlparser/README.md b/sqlparser/README.md index 72c7c943..6847a21c 100644 --- a/sqlparser/README.md +++ b/sqlparser/README.md @@ -74,7 +74,6 @@ package to generate type-safe methods from sql. Most on this list is just not supported yet because I didn't found a use case for them yet. If you need them, just leave an issue and I'll try to implement them soon. -- Some advanced expressions, like `CAST`s aren't supported yet. - An `UPSERT` clause is not yet supported on insert statements If you run into parsing errors with what you think is valid sql, please create an issue. diff --git a/sqlparser/lib/src/ast/statements/create_trigger.dart b/sqlparser/lib/src/ast/statements/create_trigger.dart index 6da26955..20b5fe2e 100644 --- a/sqlparser/lib/src/ast/statements/create_trigger.dart +++ b/sqlparser/lib/src/ast/statements/create_trigger.dart @@ -48,7 +48,7 @@ class CreateTriggerStatement extends Statement implements SchemaStatement { enum TriggerMode { before, after, insteadOf } abstract class TriggerTarget { - const TriggerTarget(); + const TriggerTarget._(); @override int get hashCode => runtimeType.hashCode; @@ -57,15 +57,15 @@ abstract class TriggerTarget { } class DeleteTarget extends TriggerTarget { - const DeleteTarget(); + const DeleteTarget() : super._(); } class InsertTarget extends TriggerTarget { - const InsertTarget(); + const InsertTarget() : super._(); } class UpdateTarget extends TriggerTarget { final List columnNames; - UpdateTarget(this.columnNames); + UpdateTarget(this.columnNames) : super._(); }