mirror of https://github.com/AMT-Cheif/drift.git
Analyze body of CREATE TRIGGER statements
This commit is contained in:
parent
75cbe74b53
commit
cf6824af82
|
@ -2,17 +2,21 @@ import 'package:moor_generator/moor_generator.dart';
|
||||||
import 'package:moor_generator/src/analyzer/errors.dart';
|
import 'package:moor_generator/src/analyzer/errors.dart';
|
||||||
import 'package:moor_generator/src/analyzer/runner/results.dart';
|
import 'package:moor_generator/src/analyzer/runner/results.dart';
|
||||||
import 'package:moor_generator/src/analyzer/runner/steps.dart';
|
import 'package:moor_generator/src/analyzer/runner/steps.dart';
|
||||||
|
import 'package:moor_generator/src/analyzer/sql_queries/lints/linter.dart';
|
||||||
|
import 'package:moor_generator/src/analyzer/sql_queries/query_analyzer.dart';
|
||||||
import 'package:sqlparser/sqlparser.dart';
|
import 'package:sqlparser/sqlparser.dart';
|
||||||
|
|
||||||
/// Handles `REFERENCES` clauses in tables by resolving their columns and
|
/// Handles `REFERENCES` clauses in tables by resolving their columns and
|
||||||
/// reporting errors if they don't exist. Further, sets the
|
/// reporting errors if they don't exist. Further, sets the
|
||||||
/// [MoorTable.references] field for tables declared in moor.
|
/// [MoorTable.references] field for tables declared in moor.
|
||||||
class EntityHandler {
|
class EntityHandler extends BaseAnalyzer {
|
||||||
final AnalyzeMoorStep step;
|
|
||||||
final ParsedMoorFile file;
|
final ParsedMoorFile file;
|
||||||
final List<MoorTable> availableTables;
|
|
||||||
|
|
||||||
EntityHandler(this.step, this.file, this.availableTables);
|
AnalyzeMoorStep get moorStep => step as AnalyzeMoorStep;
|
||||||
|
|
||||||
|
EntityHandler(
|
||||||
|
AnalyzeMoorStep step, this.file, List<MoorTable> availableTables)
|
||||||
|
: super(availableTables, step);
|
||||||
|
|
||||||
final Map<CreateTriggerStatement, MoorTrigger> _triggers = {};
|
final Map<CreateTriggerStatement, MoorTrigger> _triggers = {};
|
||||||
final Map<TableInducingStatement, MoorTable> _tables = {};
|
final Map<TableInducingStatement, MoorTable> _tables = {};
|
||||||
|
@ -31,8 +35,17 @@ class EntityHandler {
|
||||||
trigger.on = null;
|
trigger.on = null;
|
||||||
|
|
||||||
final declaration = trigger.declaration as MoorTriggerDeclaration;
|
final declaration = trigger.declaration as MoorTriggerDeclaration;
|
||||||
_triggers[declaration.node] = trigger;
|
final node = declaration.node;
|
||||||
declaration.node.acceptWithoutArg(referenceResolver);
|
_triggers[node] = trigger;
|
||||||
|
node.acceptWithoutArg(referenceResolver);
|
||||||
|
|
||||||
|
// triggers can have complex statements, so run the linter on them
|
||||||
|
final context = engine.analyzeNode(node, file.parseResult.sql);
|
||||||
|
context.errors.forEach(report);
|
||||||
|
|
||||||
|
final linter = Linter(context, mapper);
|
||||||
|
linter.reportLints();
|
||||||
|
reportLints(linter.lints, name: trigger.displayName);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -51,8 +64,7 @@ class _ReferenceResolvingVisitor extends RecursiveVisitor<void, void> {
|
||||||
_ReferenceResolvingVisitor(this.handler);
|
_ReferenceResolvingVisitor(this.handler);
|
||||||
|
|
||||||
MoorTable _resolveTable(TableReference reference) {
|
MoorTable _resolveTable(TableReference reference) {
|
||||||
return handler.availableTables.singleWhere(
|
return handler.tables.singleWhere((t) => t.sqlName == reference.tableName,
|
||||||
(t) => t.sqlName == reference.tableName,
|
|
||||||
orElse: () => null);
|
orElse: () => null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -10,7 +10,7 @@ import 'package:moor_generator/src/analyzer/runner/file_graph.dart';
|
||||||
import 'package:moor_generator/src/analyzer/runner/results.dart';
|
import 'package:moor_generator/src/analyzer/runner/results.dart';
|
||||||
import 'package:moor_generator/src/analyzer/moor/inline_dart_resolver.dart';
|
import 'package:moor_generator/src/analyzer/moor/inline_dart_resolver.dart';
|
||||||
import 'package:moor_generator/src/analyzer/moor/parser.dart';
|
import 'package:moor_generator/src/analyzer/moor/parser.dart';
|
||||||
import 'package:moor_generator/src/analyzer/sql_queries/sql_parser.dart';
|
import 'package:moor_generator/src/analyzer/sql_queries/query_analyzer.dart';
|
||||||
import 'package:moor_generator/src/analyzer/sql_queries/type_mapping.dart';
|
import 'package:moor_generator/src/analyzer/sql_queries/type_mapping.dart';
|
||||||
import 'package:moor_generator/src/analyzer/runner/task.dart';
|
import 'package:moor_generator/src/analyzer/runner/task.dart';
|
||||||
import 'package:moor_generator/src/model/sql_query.dart';
|
import 'package:moor_generator/src/model/sql_query.dart';
|
||||||
|
|
|
@ -40,7 +40,8 @@ class AnalyzeDartStep extends AnalyzingStep {
|
||||||
.expand((f) => f.resolvedQueries);
|
.expand((f) => f.resolvedQueries);
|
||||||
|
|
||||||
final availableTables = availableEntities.whereType<MoorTable>().toList();
|
final availableTables = availableEntities.whereType<MoorTable>().toList();
|
||||||
final parser = SqlParser(this, availableTables, accessor.declaredQueries);
|
final parser =
|
||||||
|
SqlAnalyzer(this, availableTables, accessor.declaredQueries);
|
||||||
parser.parse();
|
parser.parse();
|
||||||
|
|
||||||
accessor
|
accessor
|
||||||
|
|
|
@ -13,7 +13,7 @@ class AnalyzeMoorStep extends AnalyzingStep {
|
||||||
.followedBy(parseResult.declaredTables)
|
.followedBy(parseResult.declaredTables)
|
||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
final parser = SqlParser(this, availableTables, parseResult.queries)
|
final parser = SqlAnalyzer(this, availableTables, parseResult.queries)
|
||||||
..parse();
|
..parse();
|
||||||
|
|
||||||
EntityHandler(this, parseResult, availableTables).handle();
|
EntityHandler(this, parseResult, availableTables).handle();
|
||||||
|
|
|
@ -1,17 +1,23 @@
|
||||||
import 'package:sqlparser/sqlparser.dart';
|
import 'package:sqlparser/sqlparser.dart';
|
||||||
|
|
||||||
import '../query_handler.dart';
|
import '../query_handler.dart';
|
||||||
|
import '../type_mapping.dart';
|
||||||
|
|
||||||
/// Provides additional hints that aren't implemented in the sqlparser because
|
/// Provides additional hints that aren't implemented in the sqlparser because
|
||||||
/// they're specific to moor.
|
/// they're specific to moor.
|
||||||
class Linter {
|
class Linter {
|
||||||
final QueryHandler handler;
|
final AnalysisContext context;
|
||||||
|
final TypeMapper mapper;
|
||||||
final List<AnalysisError> lints = [];
|
final List<AnalysisError> lints = [];
|
||||||
|
|
||||||
Linter(this.handler);
|
Linter(this.context, this.mapper);
|
||||||
|
|
||||||
|
Linter.forHandler(QueryHandler handler)
|
||||||
|
: context = handler.context,
|
||||||
|
mapper = handler.mapper;
|
||||||
|
|
||||||
void reportLints() {
|
void reportLints() {
|
||||||
handler.context.root.acceptWithoutArg(_LintingVisitor(this));
|
context.root.acceptWithoutArg(_LintingVisitor(this));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -29,7 +35,7 @@ class _LintingVisitor extends RecursiveVisitor<void, void> {
|
||||||
// to know the column name (e.g. it's a Dart template without an AS), or
|
// to know the column name (e.g. it's a Dart template without an AS), or
|
||||||
// if the type is unknown.
|
// if the type is unknown.
|
||||||
final expression = e.expression;
|
final expression = e.expression;
|
||||||
final resolveResult = linter.handler.context.typeOf(expression);
|
final resolveResult = linter.context.typeOf(expression);
|
||||||
|
|
||||||
if (resolveResult.type == null) {
|
if (resolveResult.type == null) {
|
||||||
linter.lints.add(AnalysisError(
|
linter.lints.add(AnalysisError(
|
||||||
|
@ -86,8 +92,7 @@ class _LintingVisitor extends RecursiveVisitor<void, void> {
|
||||||
);
|
);
|
||||||
|
|
||||||
// second, check that no required columns are left out
|
// second, check that no required columns are left out
|
||||||
final specifiedTable =
|
final specifiedTable = linter.mapper.tableToMoor(e.table.resolved as Table);
|
||||||
linter.handler.mapper.tableToMoor(e.table.resolved as Table);
|
|
||||||
final required =
|
final required =
|
||||||
specifiedTable.columns.where((c) => c.requiredDuringInsert).toList();
|
specifiedTable.columns.where((c) => c.requiredDuringInsert).toList();
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import 'package:build/build.dart';
|
import 'package:build/build.dart';
|
||||||
|
import 'package:meta/meta.dart';
|
||||||
import 'package:moor_generator/moor_generator.dart';
|
import 'package:moor_generator/moor_generator.dart';
|
||||||
import 'package:moor_generator/src/analyzer/errors.dart';
|
import 'package:moor_generator/src/analyzer/errors.dart';
|
||||||
import 'package:moor_generator/src/analyzer/runner/file_graph.dart';
|
import 'package:moor_generator/src/analyzer/runner/file_graph.dart';
|
||||||
|
@ -8,27 +9,59 @@ import 'package:moor_generator/src/analyzer/sql_queries/query_handler.dart';
|
||||||
import 'package:moor_generator/src/analyzer/sql_queries/type_mapping.dart';
|
import 'package:moor_generator/src/analyzer/sql_queries/type_mapping.dart';
|
||||||
import 'package:sqlparser/sqlparser.dart' hide ResultColumn;
|
import 'package:sqlparser/sqlparser.dart' hide ResultColumn;
|
||||||
|
|
||||||
class SqlParser {
|
abstract class BaseAnalyzer {
|
||||||
final List<MoorTable> tables;
|
final List<MoorTable> tables;
|
||||||
final Step step;
|
final Step step;
|
||||||
final List<DeclaredQuery> definedQueries;
|
|
||||||
|
|
||||||
final TypeMapper _mapper = TypeMapper();
|
@protected
|
||||||
|
final TypeMapper mapper = TypeMapper();
|
||||||
SqlEngine _engine;
|
SqlEngine _engine;
|
||||||
|
|
||||||
|
BaseAnalyzer(this.tables, this.step);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
SqlEngine get engine {
|
||||||
|
if (_engine == null) {
|
||||||
|
_engine = step.task.session.spawnEngine();
|
||||||
|
tables.map(mapper.extractStructure).forEach(_engine.registerTable);
|
||||||
|
}
|
||||||
|
return _engine;
|
||||||
|
}
|
||||||
|
|
||||||
|
@protected
|
||||||
|
void report(AnalysisError error, {String Function() msg, Severity severity}) {
|
||||||
|
if (step.file.type == FileType.moor) {
|
||||||
|
step.reportError(
|
||||||
|
ErrorInMoorFile.fromSqlParser(error, overrideSeverity: severity));
|
||||||
|
} else {
|
||||||
|
step.reportError(MoorError(
|
||||||
|
severity: severity,
|
||||||
|
message: msg(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@protected
|
||||||
|
void reportLints(List<AnalysisError> lints, {String name}) {
|
||||||
|
for (final lint in lints) {
|
||||||
|
report(
|
||||||
|
lint,
|
||||||
|
msg: () => 'Lint for $name: $lint',
|
||||||
|
severity: Severity.warning,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class SqlAnalyzer extends BaseAnalyzer {
|
||||||
|
final List<DeclaredQuery> definedQueries;
|
||||||
|
|
||||||
final List<SqlQuery> foundQueries = [];
|
final List<SqlQuery> foundQueries = [];
|
||||||
|
|
||||||
SqlParser(this.step, this.tables, this.definedQueries) {
|
SqlAnalyzer(Step step, List<MoorTable> tables, this.definedQueries)
|
||||||
_engine = step.task.session.spawnEngine();
|
: super(tables, step);
|
||||||
}
|
|
||||||
|
|
||||||
void _spawnEngine() {
|
|
||||||
tables.map(_mapper.extractStructure).forEach(_engine.registerTable);
|
|
||||||
}
|
|
||||||
|
|
||||||
void parse() {
|
void parse() {
|
||||||
_spawnEngine();
|
|
||||||
|
|
||||||
for (final query in definedQueries) {
|
for (final query in definedQueries) {
|
||||||
final name = query.name;
|
final name = query.name;
|
||||||
var declaredInMoor = false;
|
var declaredInMoor = false;
|
||||||
|
@ -38,9 +71,9 @@ class SqlParser {
|
||||||
try {
|
try {
|
||||||
if (query is DeclaredDartQuery) {
|
if (query is DeclaredDartQuery) {
|
||||||
final sql = query.sql;
|
final sql = query.sql;
|
||||||
context = _engine.analyze(sql);
|
context = engine.analyze(sql);
|
||||||
} else if (query is DeclaredMoorQuery) {
|
} else if (query is DeclaredMoorQuery) {
|
||||||
context = _engine.analyzeNode(
|
context = engine.analyzeNode(
|
||||||
query.query,
|
query.query,
|
||||||
query.file.parseResult.sql,
|
query.file.parseResult.sql,
|
||||||
stmtOptions: _createOptions(query.astNode),
|
stmtOptions: _createOptions(query.astNode),
|
||||||
|
@ -55,13 +88,13 @@ class SqlParser {
|
||||||
}
|
}
|
||||||
|
|
||||||
for (final error in context.errors) {
|
for (final error in context.errors) {
|
||||||
_report(error,
|
report(error,
|
||||||
msg: () => 'The sql query $name is invalid: $error',
|
msg: () => 'The sql query $name is invalid: $error',
|
||||||
severity: Severity.error);
|
severity: Severity.error);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final query = QueryHandler(name, context, _mapper).handle()
|
final query = QueryHandler(name, context, mapper).handle()
|
||||||
..declaredInMoorFile = declaredInMoor;
|
..declaredInMoorFile = declaredInMoor;
|
||||||
foundQueries.add(query);
|
foundQueries.add(query);
|
||||||
} catch (e, s) {
|
} catch (e, s) {
|
||||||
|
@ -72,29 +105,12 @@ class SqlParser {
|
||||||
|
|
||||||
// report lints
|
// report lints
|
||||||
for (final query in foundQueries) {
|
for (final query in foundQueries) {
|
||||||
for (final lint in query.lints) {
|
reportLints(query.lints, name: query.name);
|
||||||
_report(lint,
|
|
||||||
msg: () => 'Lint for ${query.name}: $lint',
|
|
||||||
severity: Severity.warning);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void _report(AnalysisError error,
|
|
||||||
{String Function() msg, Severity severity}) {
|
|
||||||
if (step.file.type == FileType.moor) {
|
|
||||||
step.reportError(
|
|
||||||
ErrorInMoorFile.fromSqlParser(error, overrideSeverity: severity));
|
|
||||||
} else {
|
|
||||||
step.reportError(MoorError(
|
|
||||||
severity: severity,
|
|
||||||
message: msg(),
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
AnalyzeStatementOptions _createOptions(DeclaredStatement stmt) {
|
AnalyzeStatementOptions _createOptions(DeclaredStatement stmt) {
|
||||||
final reader = _engine.schemaReader;
|
final reader = engine.schemaReader;
|
||||||
final indexedHints = <int, ResolvedType>{};
|
final indexedHints = <int, ResolvedType>{};
|
||||||
final namedHints = <String, ResolvedType>{};
|
final namedHints = <String, ResolvedType>{};
|
||||||
|
|
|
@ -30,7 +30,7 @@ class QueryHandler {
|
||||||
_verifyNoSkippedIndexes();
|
_verifyNoSkippedIndexes();
|
||||||
final query = _mapToMoor();
|
final query = _mapToMoor();
|
||||||
|
|
||||||
final linter = Linter(this);
|
final linter = Linter.forHandler(this);
|
||||||
linter.reportLints();
|
linter.reportLints();
|
||||||
query.lints = linter.lints;
|
query.lints = linter.lints;
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,11 @@
|
||||||
|
import 'package:moor_generator/src/analyzer/errors.dart';
|
||||||
import 'package:moor_generator/src/analyzer/sql_queries/query_handler.dart';
|
import 'package:moor_generator/src/analyzer/sql_queries/query_handler.dart';
|
||||||
import 'package:moor_generator/src/analyzer/sql_queries/type_mapping.dart';
|
import 'package:moor_generator/src/analyzer/sql_queries/type_mapping.dart';
|
||||||
import 'package:sqlparser/sqlparser.dart';
|
import 'package:sqlparser/sqlparser.dart';
|
||||||
import 'package:test/test.dart';
|
import 'package:test/test.dart';
|
||||||
|
|
||||||
|
import '../utils.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
final engine = SqlEngine(useMoorExtensions: true);
|
final engine = SqlEngine(useMoorExtensions: true);
|
||||||
final mapper = TypeMapper();
|
final mapper = TypeMapper();
|
||||||
|
@ -22,4 +25,48 @@ void main() {
|
||||||
expect(moorQuery.lints,
|
expect(moorQuery.lints,
|
||||||
anyElement((AnalysisError q) => q.message.contains('Dart template')));
|
anyElement((AnalysisError q) => q.message.contains('Dart template')));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
group('warns about insert column count mismatch', () {
|
||||||
|
test('in top-level queries', () async {
|
||||||
|
final state = TestState.withContent({
|
||||||
|
'foo|lib/a.moor': '''
|
||||||
|
CREATE TABLE foo (
|
||||||
|
id INT NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||||
|
context VARCHAR
|
||||||
|
);
|
||||||
|
|
||||||
|
test: INSERT INTO foo VALUES (?)
|
||||||
|
''',
|
||||||
|
});
|
||||||
|
|
||||||
|
final file = await state.analyze('package:foo/a.moor');
|
||||||
|
expect(
|
||||||
|
file.errors.errors,
|
||||||
|
contains(const TypeMatcher<MoorError>().having(
|
||||||
|
(e) => e.message, 'message', 'Expected tuple to have 2 values')),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('in CREATE TRIGGER statements', () async {
|
||||||
|
final state = TestState.withContent({
|
||||||
|
'foo|lib/a.moor': '''
|
||||||
|
CREATE TABLE foo (
|
||||||
|
id INT NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||||
|
context VARCHAR
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TRIGGER my_trigger AFTER DELETE ON foo BEGIN
|
||||||
|
INSERT INTO foo VALUES (old.context);
|
||||||
|
END;
|
||||||
|
''',
|
||||||
|
});
|
||||||
|
|
||||||
|
final file = await state.analyze('package:foo/a.moor');
|
||||||
|
expect(
|
||||||
|
file.errors.errors,
|
||||||
|
contains(const TypeMatcher<MoorError>().having(
|
||||||
|
(e) => e.message, 'message', 'Expected tuple to have 2 values')),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue