diff --git a/sqlparser/CHANGELOG.md b/sqlparser/CHANGELOG.md
index a0034530..179fc6de 100644
--- a/sqlparser/CHANGELOG.md
+++ b/sqlparser/CHANGELOG.md
@@ -6,6 +6,7 @@
- Breaking: Removed `visitQueryable`. Use `defaultQueryable` instead.
- Support parsing and analyzing `CREATE VIEW` statements (see `SchemaFromCreateTable.readView`).
Thanks to [@mqus](https://github.com/mqus) for their contribution!
+- `SqlEngine.parse` will no longer throw when there's a parsing error (use `ParseResult.errors` instead).
## 0.9.0
diff --git a/sqlparser/lib/src/analysis/error.dart b/sqlparser/lib/src/analysis/error.dart
index 48b378d3..e600fd0b 100644
--- a/sqlparser/lib/src/analysis/error.dart
+++ b/sqlparser/lib/src/analysis/error.dart
@@ -1,23 +1,31 @@
part of 'analysis.dart';
class AnalysisError {
- final AstNode relevantNode;
+ final SyntacticEntity source;
+
final String message;
final AnalysisErrorType type;
- AnalysisError({@required this.type, this.message, this.relevantNode});
+ AnalysisError._internal(this.type, this.message, this.source);
+
+ AnalysisError({@required this.type, this.message, AstNode relevantNode})
+ : source = relevantNode;
+
+ @Deprecated('Use source instead')
+ AstNode get relevantNode => source as AstNode;
+
+ factory AnalysisError.fromParser(ParsingError error) {
+ return AnalysisError._internal(
+ AnalysisErrorType.synctactic,
+ error.message,
+ error.token,
+ );
+ }
/// The relevant portion of the source code that caused this error. Some AST
- /// nodes don't have a span, in that case this error is going to be null.
- FileSpan get span {
- final first = relevantNode?.first?.span;
- final last = relevantNode?.last?.span;
-
- if (first != null && last != null) {
- return first.expand(last);
- }
- return null;
- }
+ /// nodes don't have a span, in that case this error is going to have a null
+ /// span as well.
+ FileSpan get span => source.span;
@override
String toString() {
@@ -54,8 +62,6 @@ enum AnalysisErrorType {
referencedUnknownTable,
referencedUnknownColumn,
ambiguousReference,
-
- /// Note that most syntax errors are reported as [ParsingError]
synctactic,
unknownFunction,
diff --git a/sqlparser/lib/src/ast/ast.dart b/sqlparser/lib/src/ast/ast.dart
index 06562df1..55def297 100644
--- a/sqlparser/lib/src/ast/ast.dart
+++ b/sqlparser/lib/src/ast/ast.dart
@@ -37,6 +37,7 @@ part 'statements/create_trigger.dart';
part 'statements/create_view.dart';
part 'statements/delete.dart';
part 'statements/insert.dart';
+part 'statements/invalid.dart';
part 'statements/select.dart';
part 'statements/statement.dart';
part 'statements/update.dart';
diff --git a/sqlparser/lib/src/ast/statements/invalid.dart b/sqlparser/lib/src/ast/statements/invalid.dart
new file mode 100644
index 00000000..5a43b579
--- /dev/null
+++ b/sqlparser/lib/src/ast/statements/invalid.dart
@@ -0,0 +1,18 @@
+part of '../ast.dart';
+
+/// Used as a top-level parsing
+class InvalidStatement extends Statement {
+ @override
+ R accept(AstVisitor visitor, A arg) {
+ return visitor.visitInvalidStatement(this, arg);
+ }
+
+ @override
+ Iterable get childNodes => const Iterable.empty();
+
+ @override
+ bool contentEquals(InvalidStatement other) => true;
+
+ @override
+ void transformChildren(Transformer transformer, A arg) {}
+}
diff --git a/sqlparser/lib/src/ast/visitor.dart b/sqlparser/lib/src/ast/visitor.dart
index bb36874a..5bb4653b 100644
--- a/sqlparser/lib/src/ast/visitor.dart
+++ b/sqlparser/lib/src/ast/visitor.dart
@@ -14,6 +14,7 @@ abstract class AstVisitor {
R visitCreateTriggerStatement(CreateTriggerStatement e, A arg);
R visitCreateIndexStatement(CreateIndexStatement e, A arg);
R visitCreateViewStatement(CreateViewStatement e, A arg);
+ R visitInvalidStatement(InvalidStatement e, A arg);
R visitWithClause(WithClause e, A arg);
R visitUpsertClause(UpsertClause e, A arg);
@@ -96,6 +97,11 @@ abstract class AstVisitor {
class RecursiveVisitor implements AstVisitor {
// Statements
+ @override
+ R visitInvalidStatement(InvalidStatement e, A arg) {
+ return visitStatement(e, arg);
+ }
+
@override
R visitSelectStatement(SelectStatement e, A arg) {
return visitBaseSelectStatement(e, arg);
diff --git a/sqlparser/lib/src/engine/sql_engine.dart b/sqlparser/lib/src/engine/sql_engine.dart
index 65a95f3b..17f71a6a 100644
--- a/sqlparser/lib/src/engine/sql_engine.dart
+++ b/sqlparser/lib/src/engine/sql_engine.dart
@@ -110,7 +110,7 @@ class SqlEngine {
final tokensForParser = tokens.where((t) => !t.invisibleToParser).toList();
final parser = Parser(tokensForParser, useMoor: options.useMoorExtensions);
- final stmt = parser.statement();
+ final stmt = parser.safeStatement();
return ParseResult._(stmt, tokens, parser.errors, sql, null);
}
@@ -143,7 +143,14 @@ class SqlEngine {
/// this statement only.
AnalysisContext analyze(String sql, {AnalyzeStatementOptions stmtOptions}) {
final result = parse(sql);
- return analyzeParsed(result, stmtOptions: stmtOptions);
+ final analyzed = analyzeParsed(result, stmtOptions: stmtOptions);
+
+ // Add parsing errors that occured to the beginning since they are the most
+ // prominent problems.
+ analyzed.errors
+ .insertAll(0, result.errors.map((e) => AnalysisError.fromParser(e)));
+
+ return analyzed;
}
/// Analyzes a parsed [result] statement. The [AnalysisContext] returned
diff --git a/sqlparser/lib/src/reader/parser/parser.dart b/sqlparser/lib/src/reader/parser/parser.dart
index b75d6102..2a7d81ba 100644
--- a/sqlparser/lib/src/reader/parser/parser.dart
+++ b/sqlparser/lib/src/reader/parser/parser.dart
@@ -225,6 +225,12 @@ class Parser extends ParserBase
// todo remove this and don't be that lazy in moorFile()
var _lastStmtHadParsingError = false;
+ /// Parses a statement without throwing when there's a parsing error.
+ Statement /*?*/ safeStatement() {
+ return _parseAsStatement(statement, requireSemicolon: false) ??
+ InvalidStatement();
+ }
+
Statement statement() {
final first = _peek;
Statement stmt = _crud();
@@ -369,14 +375,15 @@ class Parser extends ParserBase
/// Invokes [parser], sets the appropriate source span and attaches a
/// semicolon if one exists.
- T _parseAsStatement(T Function() parser) {
+ T _parseAsStatement(T Function() parser,
+ {bool requireSemicolon = true}) {
_lastStmtHadParsingError = false;
final first = _peek;
T result;
try {
result = parser();
- if (result != null) {
+ if (result != null && requireSemicolon) {
result.semicolon = _consume(TokenType.semicolon,
'Expected a semicolon after the statement ended');
result.setSpan(first, _previous);
diff --git a/sqlparser/test/engine/sql_engine_test.dart b/sqlparser/test/engine/sql_engine_test.dart
new file mode 100644
index 00000000..779095a7
--- /dev/null
+++ b/sqlparser/test/engine/sql_engine_test.dart
@@ -0,0 +1,30 @@
+import 'package:sqlparser/sqlparser.dart';
+import 'package:test/test.dart';
+
+void main() {
+ test('does not throw when parsing invalid statements', () {
+ final engine = SqlEngine();
+ ParseResult result;
+
+ try {
+ result = engine.parse('UDPATE foo SET bar = foo;');
+ } on ParsingError {
+ fail('Calling engine.parse threw an error');
+ }
+
+ expect(result.errors, isNotEmpty);
+ });
+
+ test('does not throw when analyzing invalid statements', () {
+ final engine = SqlEngine();
+ AnalysisContext result;
+
+ try {
+ result = engine.analyze('UDPATE foo SET bar = foo;');
+ } on ParsingError {
+ fail('Calling engine.parse threw an error');
+ }
+
+ expect(result.errors, isNotEmpty);
+ });
+}
diff --git a/sqlparser/test/parser/create_table_test.dart b/sqlparser/test/parser/create_table_test.dart
index 435d3c90..8ed36390 100644
--- a/sqlparser/test/parser/create_table_test.dart
+++ b/sqlparser/test/parser/create_table_test.dart
@@ -188,16 +188,16 @@ void main() {
test("can't have empty arguments in CREATE VIRTUAL TABLE", () {
final engine = SqlEngine();
expect(
- () => engine.parse('CREATE VIRTUAL TABLE foo USING bar(a,)'),
- throwsA(
+ engine.parse('CREATE VIRTUAL TABLE foo USING bar(a,)').errors,
+ contains(
const TypeMatcher()
.having((e) => e.token.lexeme, 'fails at closing bracket', ')'),
),
);
expect(
- () => engine.parse('CREATE VIRTUAL TABLE foo USING bar(a,,b)'),
- throwsA(
+ engine.parse('CREATE VIRTUAL TABLE foo USING bar(a,,b)').errors,
+ contains(
const TypeMatcher()
.having((e) => e.token.lexeme, 'fails at next comma', ','),
),