Skip to content

Commit 273b083

Browse files
committed
Better recovery for selects without columns
1 parent 49a37f2 commit 273b083

4 files changed

Lines changed: 58 additions & 7 deletions

File tree

sqlparser/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## 0.44.2
2+
3+
- Better error recovery for select statements without result columns.
4+
15
## 0.44.1
26

37
- Expose optional table name token for `StarResultColumn`.

sqlparser/lib/src/reader/parser.dart

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1631,16 +1631,27 @@ extension Parser on ParserState {
16311631

16321632
List<ResultColumn> _resultColumns() {
16331633
final columns = <ResultColumn>[];
1634+
var isFirst = true;
1635+
16341636
do {
1635-
columns.add(_resultColumn());
1637+
// While there must be at least one column, making the first optional
1638+
// allows for better error recovery in queries like `SELECT FROM ...`.
1639+
final column = _resultColumn(optional: isFirst);
1640+
if (column == null) {
1641+
errors.add(ParsingError(_peek, 'Expected a result column here.'));
1642+
break;
1643+
}
1644+
1645+
columns.add(column);
1646+
isFirst = false;
16361647
} while (_matchOne(TokenType.comma));
16371648

16381649
return columns;
16391650
}
16401651

16411652
/// Parses a [ResultColumn] or throws if none is found.
16421653
/// https://www.sqlite.org/syntax/result-column.html
1643-
ResultColumn _resultColumn() {
1654+
ResultColumn? _resultColumn({bool optional = false}) {
16441655
if (_matchOne(TokenType.star)) {
16451656
return StarResultColumn(null)..setSpan(_previous, _previous);
16461657
}
@@ -1673,11 +1684,14 @@ extension Parser on ParserState {
16731684
Expression expr;
16741685

16751686
{
1676-
final parser = _ExpressionParser(this, allowResultColumn: true);
1687+
final parser =
1688+
_ExpressionParser(this, allowResultColumn: true, optional: optional);
16771689
try {
16781690
expr = parser._or();
16791691
} on _ParsedResultColumn catch (e) {
16801692
return e._resultColumn;
1693+
} on _NoExpressionFound {
1694+
return null;
16811695
}
16821696
}
16831697

@@ -2716,7 +2730,15 @@ final class _ExpressionParser extends ParserState {
27162730
/// instead of returning regularly if this is enabled.
27172731
final bool allowResultColumn;
27182732

2719-
_ExpressionParser(super.state, {this.allowResultColumn = false})
2733+
/// Whether the expression being parsed is optional, meaning that it's
2734+
/// acceptable for no expression to be parsed too.
2735+
///
2736+
/// The parser throws a [_NoExpressionFound] exception in this case instead of
2737+
/// emitting a syntax error.
2738+
final bool optional;
2739+
2740+
_ExpressionParser(super.state,
2741+
{this.allowResultColumn = false, this.optional = false})
27202742
: super.fromParent();
27212743

27222744
/// Parses an expression of the form `a <T> b`, where `<T>` is in [types] and
@@ -3066,6 +3088,10 @@ final class _ExpressionParser extends ParserState {
30663088
return _referenceOrFunctionCall();
30673089
}
30683090

3091+
if (optional) {
3092+
throw const _NoExpressionFound();
3093+
}
3094+
30693095
if (_peek is KeywordToken) {
30703096
// Improve error messages for possible function calls, https://github.com/simolus3/drift/discussions/2277
30713097
final (_, next) = tokens.keywordLookahead2();
@@ -3240,6 +3266,10 @@ final class _ParsedResultColumn implements Exception {
32403266
_ParsedResultColumn(this._resultColumn);
32413267
}
32423268

3269+
final class _NoExpressionFound implements Exception {
3270+
const _NoExpressionFound();
3271+
}
3272+
32433273
extension on List<Token> {
32443274
String get lexeme => first.span.expand(last.span).text;
32453275
}

sqlparser/pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
name: sqlparser
22
description: Parses sqlite statements and performs static analysis on them
3-
version: 0.44.1
3+
version: 0.44.2
44
homepage: https://github.com/simolus3/drift/tree/develop/sqlparser
55
repository: https://github.com/simolus3/drift
66
#homepage: https://drift.simonbinder.eu/

sqlparser/test/parser/errors_test.dart

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import 'package:sqlparser/sqlparser.dart';
2+
import 'package:sqlparser/src/utils/ast_equality.dart';
23
import 'package:test/test.dart';
34

45
import 'utils.dart';
@@ -21,7 +22,7 @@ void main() {
2122

2223
group('when using keywords', () {
2324
test('for function calls', () {
24-
expectError('SELECT replace(a, b, c);', [
25+
expectError('SELECT 1, replace(a, b, c);', [
2526
isParsingError(
2627
message: contains('Did you mean to call a function?'),
2728
span: 'replace',
@@ -30,7 +31,7 @@ void main() {
3031
});
3132

3233
test('as identifiers', () {
33-
expectError('SELECT group FROM foo;', [
34+
expectError('SELECT * FROM foo WHERE group;', [
3435
isParsingError(
3536
message: contains('Did you mean to use it as a column?'),
3637
span: 'group',
@@ -53,6 +54,22 @@ void main() {
5354
]);
5455
});
5556
});
57+
58+
test('missing result columns', () {
59+
final parsed =
60+
SqlEngine().parse(ParserEntrypoint.statement, 'SELECT FROM users;');
61+
62+
enforceEqual(
63+
parsed.rootNode,
64+
SelectStatement(
65+
columns: [],
66+
from: TableReference('users'),
67+
),
68+
);
69+
expect(parsed.errors, [
70+
isParsingError(message: 'Expected a result column here.', span: 'FROM')
71+
]);
72+
});
5673
}
5774

5875
void expectError(String sql, errorsMatcher) {

0 commit comments

Comments
 (0)