Parse compound select statements

This commit is contained in:
Simon Binder 2019-09-25 14:58:44 +02:00
parent d9c2b5f342
commit 0cbac2ee37
No known key found for this signature in database
GPG Key ID: 7891917E4147B8C0
8 changed files with 192 additions and 19 deletions

View File

@ -20,7 +20,7 @@ class QueryHandler {
Iterable<FoundVariable> get _foundVariables =>
_foundElements.whereType<FoundVariable>();
SelectStatement get _select => context.root as SelectStatement;
BaseSelectStatement get _select => context.root as BaseSelectStatement;
QueryHandler(this.name, this.context, this.mapper);
@ -39,7 +39,7 @@ class QueryHandler {
SqlQuery _mapToMoor() {
final root = context.root;
if (root is SelectStatement) {
if (root is BaseSelectStatement) {
return _handleSelect();
} else if (root is UpdateStatement ||
root is DeleteStatement ||

View File

@ -152,6 +152,8 @@ abstract class AstNode {
abstract class AstVisitor<T> {
T visitSelectStatement(SelectStatement e);
T visitCompoundSelectStatement(CompoundSelectStatement e);
T visitCompoundSelectPart(CompoundSelectPart e);
T visitResultColumn(ResultColumn e);
T visitInsertStatement(InsertStatement e);
T visitDeleteStatement(DeleteStatement e);
@ -274,6 +276,12 @@ class RecursiveVisitor<T> extends AstVisitor<T> {
@override
T visitSelectStatement(SelectStatement e) => visitChildren(e);
@override
T visitCompoundSelectStatement(CompoundSelectStatement e) => visitChildren(e);
@override
T visitCompoundSelectPart(CompoundSelectPart e) => visitChildren(e);
@override
T visitInsertStatement(InsertStatement e) => visitChildren(e);

View File

@ -1,8 +1,14 @@
part of '../ast.dart';
class SelectStatement extends Statement
with CrudStatement, ResultSet
implements HasWhereClause {
abstract class BaseSelectStatement extends Statement
with CrudStatement, ResultSet {
/// The resolved list of columns returned by this select statements. Not
/// available from the parse tree, will be set later by the analyzer.
@override
List<Column> resolvedColumns;
}
class SelectStatement extends BaseSelectStatement implements HasWhereClause {
final bool distinct;
final List<ResultColumn> columns;
final List<Queryable> from;
@ -15,11 +21,6 @@ class SelectStatement extends Statement
final OrderByBase orderBy;
final LimitBase limit;
/// The resolved list of columns returned by this select statements. Not
/// available from the parse tree, will be set later by the analyzer.
@override
List<Column> resolvedColumns;
SelectStatement(
{this.distinct = false,
this.columns,
@ -54,6 +55,36 @@ class SelectStatement extends Statement
}
}
class CompoundSelectStatement extends BaseSelectStatement {
final SelectStatement base;
final List<CompoundSelectPart> additional;
// the grammar under https://www.sqlite.org/syntax/compound-select-stmt.html
// defines an order by and limit clause on this node, but we parse them as
// part of the last compound select statement in [additional]
CompoundSelectStatement({
@required this.base,
this.additional = const [],
});
@override
Iterable<AstNode> get childNodes {
return [base, ...additional];
}
@override
T accept<T>(AstVisitor<T> visitor) {
return visitor.visitCompoundSelectStatement(this);
}
@override
bool contentEquals(CompoundSelectStatement other) {
// this class doesn't contain anything but child nodes
return true;
}
}
abstract class ResultColumn extends AstNode {
@override
T accept<T>(AstVisitor<T> visitor) => visitor.visitResultColumn(this);
@ -110,3 +141,32 @@ class GroupBy extends AstNode {
return true; // Defined via child nodes
}
}
enum CompoundSelectMode {
union,
unionAll,
intersect,
except,
}
class CompoundSelectPart extends AstNode {
final CompoundSelectMode mode;
final SelectStatement select;
/// The first token of this statement, so either union, intersect or except.
Token firstModeToken;
/// The "ALL" token, if this is a "UNION ALL" part
Token allToken;
CompoundSelectPart({@required this.mode, @required this.select});
@override
Iterable<AstNode> get childNodes => [select];
@override
T accept<T>(AstVisitor<T> visitor) => visitor.visitCompoundSelectPart(this);
@override
bool contentEquals(CompoundSelectPart other) => mode == other.mode;
}

View File

@ -2,7 +2,35 @@ part of 'parser.dart';
mixin CrudParser on ParserBase {
@override
SelectStatement select() {
BaseSelectStatement select({bool noCompound}) {
if (noCompound == true) {
return _selectNoCompound();
} else {
final first = _selectNoCompound();
final parts = <CompoundSelectPart>[];
while (true) {
final part = _compoundSelectPart();
if (part != null) {
parts.add(part);
} else {
break;
}
}
if (parts.isEmpty) {
// no compound parts, just return the simple select statement
return first;
} else {
return CompoundSelectStatement(
base: first,
additional: parts,
)..setSpan(first.first, _previous);
}
}
}
SelectStatement _selectNoCompound() {
if (!_match(const [TokenType.select])) return null;
final selectToken = _previous;
@ -38,6 +66,35 @@ mixin CrudParser on ParserBase {
)..setSpan(selectToken, _previous);
}
CompoundSelectPart _compoundSelectPart() {
if (_match(
const [TokenType.union, TokenType.intersect, TokenType.except])) {
final firstModeToken = _previous;
var mode = const {
TokenType.union: CompoundSelectMode.union,
TokenType.intersect: CompoundSelectMode.intersect,
TokenType.except: CompoundSelectMode.except,
}[firstModeToken.type];
Token allToken;
if (firstModeToken.type == TokenType.union && _matchOne(TokenType.all)) {
allToken = _previous;
mode = CompoundSelectMode.unionAll;
}
final select = _selectNoCompound();
return CompoundSelectPart(
mode: mode,
select: select,
)
..firstModeToken = firstModeToken
..allToken = allToken
..setSpan(firstModeToken, _previous);
}
return null;
}
/// Parses a [ResultColumn] or throws if none is found.
/// https://www.sqlite.org/syntax/result-column.html
ResultColumn _resultColumn() {
@ -114,7 +171,7 @@ mixin CrudParser on ParserBase {
if (tableRef != null) {
return tableRef;
} else if (_matchOne(TokenType.leftParen)) {
final innerStmt = select();
final innerStmt = _selectNoCompound();
_consume(TokenType.rightParen,
'Expected a right bracket to terminate the inner select');
@ -475,7 +532,7 @@ mixin CrudParser on ParserBase {
_consume(TokenType.$values, 'Expected DEFAULT VALUES');
return const DefaultValues();
} else {
return SelectInsertSource(select());
return SelectInsertSource(_selectNoCompound());
}
}

View File

@ -194,7 +194,7 @@ mixin ExpressionParser on ParserBase {
final existsToken = _previous;
_consume(
TokenType.leftParen, 'Expected opening parenthesis after EXISTS');
final selectStmt = select();
final selectStmt = select(noCompound: true) as SelectStatement;
_consume(TokenType.rightParen,
'Expected closing paranthesis to finish EXISTS expression');
return ExistsExpression(select: selectStmt)
@ -264,7 +264,7 @@ mixin ExpressionParser on ParserBase {
if (_matchOne(TokenType.leftParen)) {
final left = _previous;
if (_peek.type == TokenType.select) {
final stmt = select();
final stmt = select(noCompound: true) as SelectStatement;
_consume(TokenType.rightParen, 'Expected a closing bracket');
return SubQuery(select: stmt)..setSpan(left, _previous);
} else {
@ -379,7 +379,7 @@ mixin ExpressionParser on ParserBase {
_consume(TokenType.leftParen, 'Expected opening parenthesis for tuple');
final expressions = <Expression>[];
final subQuery = select();
final subQuery = select(noCompound: true) as SelectStatement;
if (subQuery == null) {
// no sub query found. read expressions that form the tuple.
// tuples can be empty `()`, so only start parsing values when it's not

View File

@ -146,12 +146,13 @@ abstract class ParserBase {
/// (in brackets) will be accepted as well.
Expression _consumeTuple({bool orSubQuery = false});
/// Parses a [SelectStatement], or returns null if there is no select token
/// after the current position.
/// Parses a [BaseSelectStatement], which is either a [SelectStatement] or a
/// [CompoundSelectStatement]. If [noCompound] is set to true, the parser will
/// only attempt to parse a [SelectStatement].
///
/// See also:
/// https://www.sqlite.org/lang_select.html
SelectStatement select();
BaseSelectStatement select({bool noCompound});
Literal _literalOrNull();
OrderingMode _orderingModeOrNull();

View File

@ -114,6 +114,10 @@ enum TokenType {
ignore,
set,
union,
intersect,
except,
create,
table,
$if,
@ -237,6 +241,9 @@ const Map<String, TokenType> keywords = {
'TIES': TokenType.ties,
'WINDOW': TokenType.window,
'VALUES': TokenType.$values,
'UNION': TokenType.union,
'INTERSECT': TokenType.intersect,
'EXCEPT': TokenType.except,
};
/// Maps [TokenType]s which are keywords to their lexeme.

View File

@ -0,0 +1,40 @@
import 'package:sqlparser/sqlparser.dart';
import 'package:test/test.dart';
import '../utils.dart';
void main() {
test('parses compound select statements', () {
testStatement(
'SELECT * FROM tbl UNION ALL SELECT 1 EXCEPT SELECT 2',
CompoundSelectStatement(
base: SelectStatement(
columns: [StarResultColumn(null)],
from: [TableReference('tbl', null)],
),
additional: [
CompoundSelectPart(
mode: CompoundSelectMode.unionAll,
select: SelectStatement(
columns: [
ExpressionResultColumn(
expression: NumericLiteral(1, token(TokenType.numberLiteral)),
),
],
),
),
CompoundSelectPart(
mode: CompoundSelectMode.except,
select: SelectStatement(
columns: [
ExpressionResultColumn(
expression: NumericLiteral(2, token(TokenType.numberLiteral)),
),
],
),
),
],
),
);
});
}