Split parser implementation into multiple files

This commit is contained in:
Simon Binder 2019-07-27 20:47:11 +02:00
parent 1766bb3f77
commit 7b6802e1c5
No known key found for this signature in database
GPG Key ID: 7891917E4147B8C0
4 changed files with 900 additions and 872 deletions

View File

@ -0,0 +1,351 @@
part of 'parser.dart';
mixin CrudParser on ParserBase {
@override
SelectStatement select() {
if (!_match(const [TokenType.select])) return null;
final selectToken = _previous;
var distinct = false;
if (_matchOne(TokenType.distinct)) {
distinct = true;
} else if (_matchOne(TokenType.all)) {
distinct = false;
}
final resultColumns = <ResultColumn>[];
do {
resultColumns.add(_resultColumn());
} while (_match(const [TokenType.comma]));
final from = _from();
final where = _where();
final groupBy = _groupBy();
final orderBy = _orderBy();
final limit = _limit();
return SelectStatement(
distinct: distinct,
columns: resultColumns,
from: from,
where: where,
groupBy: groupBy,
orderBy: orderBy,
limit: limit,
)..setSpan(selectToken, _previous);
}
/// Parses a [ResultColumn] or throws if none is found.
/// https://www.sqlite.org/syntax/result-column.html
ResultColumn _resultColumn() {
if (_match(const [TokenType.star])) {
return StarResultColumn(null)..setSpan(_previous, _previous);
}
final positionBefore = _current;
if (_match(const [TokenType.identifier])) {
// two options. the identifier could be followed by ".*", in which case
// we have a star result column. If it's followed by anything else, it can
// still refer to a column in a table as part of a expression result column
final identifier = _previous;
if (_match(const [TokenType.dot]) && _match(const [TokenType.star])) {
return StarResultColumn((identifier as IdentifierToken).identifier)
..setSpan(identifier, _previous);
}
// not a star result column. go back and parse the expression.
// todo this is a bit unorthodox. is there a better way to parse the
// expression from before?
_current = positionBefore;
}
final tokenBefore = _peek;
final expr = expression();
final as = _as();
return ExpressionResultColumn(expression: expr, as: as?.identifier)
..setSpan(tokenBefore, _previous);
}
/// Returns an identifier followed after an optional "AS" token in sql.
/// Returns null if there is
IdentifierToken _as() {
if (_match(const [TokenType.as])) {
return _consume(TokenType.identifier, 'Expected an identifier')
as IdentifierToken;
} else if (_match(const [TokenType.identifier])) {
return _previous as IdentifierToken;
} else {
return null;
}
}
List<Queryable> _from() {
if (!_matchOne(TokenType.from)) return [];
// Can either be a list of <TableOrSubquery> or a join. Joins also start
// with a TableOrSubquery, so let's first parse that.
final start = _tableOrSubquery();
// parse join, if it is one
final join = _joinClause(start);
if (join != null) {
return [join];
}
// not a join. Keep the TableOrSubqueries coming!
final queries = [start];
while (_matchOne(TokenType.comma)) {
queries.add(_tableOrSubquery());
}
return queries;
}
TableOrSubquery _tableOrSubquery() {
// this is what we're parsing: https://www.sqlite.org/syntax/table-or-subquery.html
// we currently only support regular tables and nested selects
final tableRef = _tableReference();
if (tableRef != null) {
return tableRef;
} else if (_matchOne(TokenType.leftParen)) {
final innerStmt = select();
_consume(TokenType.rightParen,
'Expected a right bracket to terminate the inner select');
final alias = _as();
return SelectStatementAsSource(
statement: innerStmt, as: alias?.identifier);
}
_error('Expected a table name or a nested select statement');
}
TableReference _tableReference() {
if (_matchOne(TokenType.identifier)) {
// ignore the schema name, it's not supported. Besides that, we're on the
// first branch in the diagram here
final tableName = (_previous as IdentifierToken).identifier;
final alias = _as();
return TableReference(tableName, alias?.identifier);
}
return null;
}
JoinClause _joinClause(TableOrSubquery start) {
var operator = _parseJoinOperatorNoComma();
if (operator == null) {
return null;
}
final joins = <Join>[];
while (operator != null) {
final subquery = _tableOrSubquery();
final constraint = _joinConstraint();
JoinOperator resolvedOperator;
if (operator.contains(TokenType.left)) {
resolvedOperator = operator.contains(TokenType.outer)
? JoinOperator.leftOuter
: JoinOperator.left;
} else if (operator.contains(TokenType.inner)) {
resolvedOperator = JoinOperator.inner;
} else if (operator.contains(TokenType.cross)) {
resolvedOperator = JoinOperator.cross;
} else if (operator.contains(TokenType.comma)) {
resolvedOperator = JoinOperator.comma;
} else {
resolvedOperator = JoinOperator.none;
}
joins.add(Join(
natural: operator.contains(TokenType.natural),
operator: resolvedOperator,
query: subquery,
constraint: constraint,
));
// parse the next operator, if there is more than one join
if (_matchOne(TokenType.comma)) {
operator = [TokenType.comma];
} else {
operator = _parseJoinOperatorNoComma();
}
}
return JoinClause(primary: start, joins: joins);
}
/// Parses https://www.sqlite.org/syntax/join-operator.html, minus the comma.
List<TokenType> _parseJoinOperatorNoComma() {
if (_match(_startOperators)) {
final operators = [_previous.type];
if (_previous.type == TokenType.join) {
// just join, without any specific operators
return operators;
} else {
// natural is a prefix, another operator can follow.
if (_previous.type == TokenType.natural) {
if (_match([TokenType.left, TokenType.inner, TokenType.cross])) {
operators.add(_previous.type);
}
}
if (_previous.type == TokenType.left && _matchOne(TokenType.outer)) {
operators.add(_previous.type);
}
_consume(TokenType.join, 'Expected to see a join keyword here');
return operators;
}
}
return null;
}
/// Parses https://www.sqlite.org/syntax/join-constraint.html
JoinConstraint _joinConstraint() {
if (_matchOne(TokenType.on)) {
return OnConstraint(expression: expression());
} else if (_matchOne(TokenType.using)) {
_consume(TokenType.leftParen, 'Expected an opening paranthesis');
final columnNames = <String>[];
do {
final identifier =
_consume(TokenType.identifier, 'Expected a column name');
columnNames.add((identifier as IdentifierToken).identifier);
} while (_matchOne(TokenType.comma));
_consume(TokenType.rightParen, 'Expected an closing paranthesis');
return UsingConstraint(columnNames: columnNames);
}
_error('Expected a constraint with ON or USING');
}
/// Parses a where clause if there is one at the current position
Expression _where() {
if (_match(const [TokenType.where])) {
return expression();
}
return null;
}
GroupBy _groupBy() {
if (_matchOne(TokenType.group)) {
_consume(TokenType.by, 'Expected a "BY"');
final by = <Expression>[];
Expression having;
do {
by.add(expression());
} while (_matchOne(TokenType.comma));
if (_matchOne(TokenType.having)) {
having = expression();
}
return GroupBy(by: by, having: having);
}
return null;
}
OrderBy _orderBy() {
if (_match(const [TokenType.order])) {
_consume(TokenType.by, 'Expected "BY" after "ORDER" token');
final terms = <OrderingTerm>[];
do {
terms.add(_orderingTerm());
} while (_matchOne(TokenType.comma));
return OrderBy(terms: terms);
}
return null;
}
OrderingTerm _orderingTerm() {
final expr = expression();
return OrderingTerm(expression: expr, orderingMode: _orderingModeOrNull());
}
@override
OrderingMode _orderingModeOrNull() {
if (_match(const [TokenType.asc, TokenType.desc])) {
final mode = _previous.type == TokenType.asc
? OrderingMode.ascending
: OrderingMode.descending;
return mode;
}
return null;
}
/// Parses a [Limit] clause, or returns null if there is no limit token after
/// the current position.
Limit _limit() {
if (!_matchOne(TokenType.limit)) return null;
// Unintuitive, it's "$amount OFFSET $offset", but "$offset, $amount"
// the order changes between the separator tokens.
final first = expression();
if (_matchOne(TokenType.comma)) {
final separator = _previous;
final count = expression();
return Limit(count: count, offsetSeparator: separator, offset: first);
} else if (_matchOne(TokenType.offset)) {
final separator = _previous;
final offset = expression();
return Limit(count: first, offsetSeparator: separator, offset: offset);
} else {
return Limit(count: first);
}
}
DeleteStatement _deleteStmt() {
if (!_matchOne(TokenType.delete)) return null;
_consume(TokenType.from, 'Expected a FROM here');
final table = _tableReference();
Expression where;
if (table == null) {
_error('Expected a table reference');
}
if (_matchOne(TokenType.where)) {
where = expression();
}
return DeleteStatement(from: table, where: where);
}
UpdateStatement _update() {
if (!_matchOne(TokenType.update)) return null;
FailureMode failureMode;
if (_matchOne(TokenType.or)) {
failureMode = UpdateStatement.failureModeFromToken(_advance().type);
}
final table = _tableReference();
_consume(TokenType.set, 'Expected SET after the table name');
final set = <SetComponent>[];
do {
final columnName =
_consume(TokenType.identifier, 'Expected a column name to set')
as IdentifierToken;
final reference = Reference(columnName: columnName.identifier)
..setSpan(columnName, columnName);
_consume(TokenType.equal, 'Expected = after the column name');
final expr = expression();
set.add(SetComponent(column: reference, expression: expr));
} while (_matchOne(TokenType.comma));
final where = _where();
return UpdateStatement(
or: failureMode, table: table, set: set, where: where);
}
}

View File

@ -0,0 +1,349 @@
part of 'parser.dart';
/// Parses expressions. Expressions have the following precedence:
/// - `-`, `+`, `~`, unary not
/// - `||` (concatenation)
/// - `*`, '/', '%'
/// - `+`, `-`
/// - `<<`, `>>`, `&`, `|`
/// - `<`, `<=`, `>`, `>=`
/// - `=`, `==`, `!=`, `<>`, `IS`, `IS NOT`, `IN`, `LIKE`, `GLOB`, `MATCH`,
/// `REGEXP`
/// - `AND`
/// - `OR`
/// - Case expressions
mixin ExpressionParser on ParserBase {
@override
Expression expression() {
return _case();
}
Expression _case() {
if (_matchOne(TokenType.$case)) {
final base = _check(TokenType.when) ? null : _or();
final whens = <WhenComponent>[];
Expression $else;
while (_matchOne(TokenType.when)) {
final whenExpr = _or();
_consume(TokenType.then, 'Expected THEN');
final then = _or();
whens.add(WhenComponent(when: whenExpr, then: then));
}
if (_matchOne(TokenType.$else)) {
$else = _or();
}
_consume(TokenType.end, 'Expected END to finish the case operator');
return CaseExpression(whens: whens, base: base, elseExpr: $else);
}
return _or();
}
/// Parses an expression of the form a <T> b, where <T> is in [types] and
/// both a and b are expressions with a higher precedence parsed from
/// [higherPrecedence].
Expression _parseSimpleBinary(
List<TokenType> types, Expression Function() higherPrecedence) {
var expression = higherPrecedence();
while (_match(types)) {
final operator = _previous;
final right = higherPrecedence();
expression = BinaryExpression(expression, operator, right);
}
return expression;
}
Expression _or() => _parseSimpleBinary(const [TokenType.or], _and);
Expression _and() => _parseSimpleBinary(const [TokenType.and], _in);
Expression _in() {
final left = _equals();
if (_checkWithNot(TokenType.$in)) {
final not = _matchOne(TokenType.not);
_matchOne(TokenType.$in);
var inside = _equals();
if (inside is Parentheses) {
// if we have something like x IN (3), then (3) is a tuple and not a
// parenthesis. We can only know this from the context unfortunately
inside = (inside as Parentheses).asTuple;
}
return InExpression(left: left, inside: inside, not: not);
}
return left;
}
/// Parses expressions with the "equals" precedence. This contains
/// comparisons, "IS (NOT) IN" expressions, between expressions and "like"
/// expressions.
Expression _equals() {
var expression = _comparison();
final ops = const [
TokenType.equal,
TokenType.doubleEqual,
TokenType.exclamationEqual,
TokenType.lessMore,
TokenType.$is,
];
final stringOps = const [
TokenType.like,
TokenType.glob,
TokenType.match,
TokenType.regexp,
];
while (true) {
if (_checkWithNot(TokenType.between)) {
final not = _matchOne(TokenType.not);
_consume(TokenType.between, 'expected a BETWEEN');
final lower = _comparison();
_consume(TokenType.and, 'expected AND');
final upper = _comparison();
expression = BetweenExpression(
not: not, check: expression, lower: lower, upper: upper);
} else if (_match(ops)) {
final operator = _previous;
if (operator.type == TokenType.$is) {
final not = _match(const [TokenType.not]);
// special case: is not expression
expression = IsExpression(not, expression, _comparison());
} else {
expression = BinaryExpression(expression, operator, _comparison());
}
} else if (_checkAnyWithNot(stringOps)) {
final not = _matchOne(TokenType.not);
_match(stringOps); // will consume, existence was verified with check
final operator = _previous;
final right = _comparison();
Expression escape;
if (_matchOne(TokenType.escape)) {
escape = _comparison();
}
expression = StringComparisonExpression(
not: not,
left: expression,
operator: operator,
right: right,
escape: escape);
} else {
break; // no matching operator with this precedence was found
}
}
return expression;
}
Expression _comparison() {
return _parseSimpleBinary(_comparisonOperators, _binaryOperation);
}
Expression _binaryOperation() {
return _parseSimpleBinary(_binaryOperators, _addition);
}
Expression _addition() {
return _parseSimpleBinary(const [
TokenType.plus,
TokenType.minus,
], _multiplication);
}
Expression _multiplication() {
return _parseSimpleBinary(const [
TokenType.star,
TokenType.slash,
TokenType.percent,
], _concatenation);
}
Expression _concatenation() {
return _parseSimpleBinary(const [TokenType.doublePipe], _unary);
}
Expression _unary() {
if (_match(const [
TokenType.minus,
TokenType.plus,
TokenType.tilde,
TokenType.not,
])) {
final operator = _previous;
final expression = _unary();
return UnaryExpression(operator, expression);
} else if (_matchOne(TokenType.exists)) {
_consume(
TokenType.leftParen, 'Expected opening parenthesis after EXISTS');
final selectStmt = select();
_consume(TokenType.rightParen,
'Expected closing paranthesis to finish EXISTS expression');
return ExistsExpression(select: selectStmt);
}
return _postfix();
}
Expression _postfix() {
// todo parse ISNULL, NOTNULL, NOT NULL, etc.
// I don't even know the precedence ¯\_(ツ)_/¯ (probably not higher than
// unary)
var expression = _primary();
while (_matchOne(TokenType.collate)) {
final collateOp = _previous;
final collateFun =
_consume(TokenType.identifier, 'Expected a collating sequence')
as IdentifierToken;
expression = CollateExpression(
inner: expression,
operator: collateOp,
collateFunction: collateFun,
)..setSpan(expression.first, collateFun);
}
return expression;
}
@override
Literal _literalOrNull() {
final token = _peek;
Literal _parseInner() {
if (_matchOne(TokenType.numberLiteral)) {
return NumericLiteral(_parseNumber(token.lexeme), token);
}
if (_matchOne(TokenType.stringLiteral)) {
return StringLiteral(token as StringLiteralToken);
}
if (_matchOne(TokenType.$null)) {
return NullLiteral(token);
}
if (_matchOne(TokenType.$true)) {
return BooleanLiteral.withTrue(token);
}
if (_matchOne(TokenType.$false)) {
return BooleanLiteral.withFalse(token);
}
// todo CURRENT_TIME, CURRENT_DATE, CURRENT_TIMESTAMP
return null;
}
final literal = _parseInner();
literal?.setSpan(token, token);
return literal;
}
Expression _primary() {
final literal = _literalOrNull();
if (literal != null) return literal;
final token = _advance();
final type = token.type;
switch (type) {
case TokenType.leftParen:
// Opening brackets could be three things: An inner select statement
// (SELECT ...), a parenthesised expression, or a tuple of expressions
// (a, b, c).
final left = token;
if (_peek.type == TokenType.select) {
final stmt = select();
_consume(TokenType.rightParen, 'Expected a closing bracket');
return SubQuery(select: stmt);
} else {
// alright, it's either a tuple or just parenthesis. A tuple can be
// empty, so if the next statement is the closing bracket we're done
if (_matchOne(TokenType.rightParen)) {
return TupleExpression(expressions: [])..setSpan(left, _previous);
}
final expr = expression();
// Are we witnessing a tuple?
if (_check(TokenType.comma)) {
// we are, add expressions as long as we see commas
final exprs = [expr];
while (_matchOne(TokenType.comma)) {
exprs.add(expression());
}
_consume(TokenType.rightParen, 'Expected a closing bracket');
return TupleExpression(expressions: exprs);
} else {
// we aren't, so that'll just be parentheses.
_consume(TokenType.rightParen, 'Expected a closing bracket');
return Parentheses(left, expr, token);
}
}
break;
case TokenType.identifier:
// could be table.column, function(...) or just column
final first = token as IdentifierToken;
if (_matchOne(TokenType.dot)) {
final second =
_consume(TokenType.identifier, 'Expected a column name here')
as IdentifierToken;
return Reference(
tableName: first.identifier, columnName: second.identifier)
..setSpan(first, second);
} else if (_matchOne(TokenType.leftParen)) {
final parameters = _functionParameters();
final rightParen = _consume(TokenType.rightParen,
'Expected closing bracket after argument list');
return FunctionExpression(
name: first.identifier, parameters: parameters)
..setSpan(first, rightParen);
} else {
return Reference(columnName: first.identifier)..setSpan(first, first);
}
break;
case TokenType.questionMark:
final mark = token;
if (_matchOne(TokenType.numberLiteral)) {
final number = _previous;
return NumberedVariable(mark, _parseNumber(number.lexeme).toInt())
..setSpan(mark, number);
} else {
return NumberedVariable(mark, null)..setSpan(mark, mark);
}
break;
case TokenType.colon:
final colon = token;
final identifier = _consume(TokenType.identifier,
'Expected an identifier for the named variable') as IdentifierToken;
final content = identifier.identifier;
return ColonNamedVariable(':$content')..setSpan(colon, identifier);
default:
break;
}
// nothing found -> issue error
_error('Could not parse this expression');
}
FunctionParameters _functionParameters() {
if (_matchOne(TokenType.star)) {
return const StarFunctionParameter();
}
final distinct = _matchOne(TokenType.distinct);
final parameters = <Expression>[];
while (_peek.type != TokenType.rightParen) {
parameters.add(expression());
}
return ExprFunctionParameters(distinct: distinct, parameters: parameters);
}
}

View File

@ -2,7 +2,10 @@ import 'package:meta/meta.dart';
import 'package:sqlparser/src/ast/ast.dart';
import 'package:sqlparser/src/reader/tokenizer/token.dart';
part 'crud.dart';
part 'num_parser.dart';
part 'expressions.dart';
part 'schema.dart';
const _comparisonOperators = [
TokenType.less,
@ -37,15 +40,12 @@ class ParsingError implements Exception {
}
}
// todo better error handling and synchronisation, like it's done here:
// https://craftinginterpreters.com/parsing-expressions.html#synchronizing-a-recursive-descent-parser
class Parser {
abstract class ParserBase {
final List<Token> tokens;
final List<ParsingError> errors = [];
int _current = 0;
Parser(this.tokens);
ParserBase(this.tokens);
bool get _isAtEnd => _peek.type == TokenType.eof;
Token get _peek => tokens[_current];
@ -109,6 +109,27 @@ class Parser {
_error(message);
}
// Common operations that we are referenced very often
Expression expression();
/// Parses a [SelectStatement], or returns null if there is no select token
/// after the current position.
///
/// See also:
/// https://www.sqlite.org/lang_select.html
SelectStatement select();
Literal _literalOrNull();
OrderingMode _orderingModeOrNull();
}
// todo better error handling and synchronisation, like it's done here:
// https://craftinginterpreters.com/parsing-expressions.html#synchronizing-a-recursive-descent-parser
class Parser extends ParserBase
with ExpressionParser, SchemaParser, CrudParser {
Parser(List<Token> tokens) : super(tokens);
Statement statement() {
final stmt = select() ?? _deleteStmt() ?? _update() ?? _createTable();
@ -118,871 +139,4 @@ class Parser {
}
return stmt;
}
/// Parses a [SelectStatement], or returns null if there is no select token
/// after the current position.
///
/// See also:
/// https://www.sqlite.org/lang_select.html
SelectStatement select() {
if (!_match(const [TokenType.select])) return null;
final selectToken = _previous;
var distinct = false;
if (_matchOne(TokenType.distinct)) {
distinct = true;
} else if (_matchOne(TokenType.all)) {
distinct = false;
}
final resultColumns = <ResultColumn>[];
do {
resultColumns.add(_resultColumn());
} while (_match(const [TokenType.comma]));
final from = _from();
final where = _where();
final groupBy = _groupBy();
final orderBy = _orderBy();
final limit = _limit();
return SelectStatement(
distinct: distinct,
columns: resultColumns,
from: from,
where: where,
groupBy: groupBy,
orderBy: orderBy,
limit: limit,
)..setSpan(selectToken, _previous);
}
/// Parses a [ResultColumn] or throws if none is found.
/// https://www.sqlite.org/syntax/result-column.html
ResultColumn _resultColumn() {
if (_match(const [TokenType.star])) {
return StarResultColumn(null)..setSpan(_previous, _previous);
}
final positionBefore = _current;
if (_match(const [TokenType.identifier])) {
// two options. the identifier could be followed by ".*", in which case
// we have a star result column. If it's followed by anything else, it can
// still refer to a column in a table as part of a expression result column
final identifier = _previous;
if (_match(const [TokenType.dot]) && _match(const [TokenType.star])) {
return StarResultColumn((identifier as IdentifierToken).identifier)
..setSpan(identifier, _previous);
}
// not a star result column. go back and parse the expression.
// todo this is a bit unorthodox. is there a better way to parse the
// expression from before?
_current = positionBefore;
}
final tokenBefore = _peek;
final expr = expression();
final as = _as();
return ExpressionResultColumn(expression: expr, as: as?.identifier)
..setSpan(tokenBefore, _previous);
}
/// Returns an identifier followed after an optional "AS" token in sql.
/// Returns null if there is
IdentifierToken _as() {
if (_match(const [TokenType.as])) {
return _consume(TokenType.identifier, 'Expected an identifier')
as IdentifierToken;
} else if (_match(const [TokenType.identifier])) {
return _previous as IdentifierToken;
} else {
return null;
}
}
List<Queryable> _from() {
if (!_matchOne(TokenType.from)) return [];
// Can either be a list of <TableOrSubquery> or a join. Joins also start
// with a TableOrSubquery, so let's first parse that.
final start = _tableOrSubquery();
// parse join, if it is one
final join = _joinClause(start);
if (join != null) {
return [join];
}
// not a join. Keep the TableOrSubqueries coming!
final queries = [start];
while (_matchOne(TokenType.comma)) {
queries.add(_tableOrSubquery());
}
return queries;
}
TableOrSubquery _tableOrSubquery() {
// this is what we're parsing: https://www.sqlite.org/syntax/table-or-subquery.html
// we currently only support regular tables and nested selects
final tableRef = _tableReference();
if (tableRef != null) {
return tableRef;
} else if (_matchOne(TokenType.leftParen)) {
final innerStmt = select();
_consume(TokenType.rightParen,
'Expected a right bracket to terminate the inner select');
final alias = _as();
return SelectStatementAsSource(
statement: innerStmt, as: alias?.identifier);
}
_error('Expected a table name or a nested select statement');
}
TableReference _tableReference() {
if (_matchOne(TokenType.identifier)) {
// ignore the schema name, it's not supported. Besides that, we're on the
// first branch in the diagram here
final tableName = (_previous as IdentifierToken).identifier;
final alias = _as();
return TableReference(tableName, alias?.identifier);
}
return null;
}
JoinClause _joinClause(TableOrSubquery start) {
var operator = _parseJoinOperatorNoComma();
if (operator == null) {
return null;
}
final joins = <Join>[];
while (operator != null) {
final subquery = _tableOrSubquery();
final constraint = _joinConstraint();
JoinOperator resolvedOperator;
if (operator.contains(TokenType.left)) {
resolvedOperator = operator.contains(TokenType.outer)
? JoinOperator.leftOuter
: JoinOperator.left;
} else if (operator.contains(TokenType.inner)) {
resolvedOperator = JoinOperator.inner;
} else if (operator.contains(TokenType.cross)) {
resolvedOperator = JoinOperator.cross;
} else if (operator.contains(TokenType.comma)) {
resolvedOperator = JoinOperator.comma;
} else {
resolvedOperator = JoinOperator.none;
}
joins.add(Join(
natural: operator.contains(TokenType.natural),
operator: resolvedOperator,
query: subquery,
constraint: constraint,
));
// parse the next operator, if there is more than one join
if (_matchOne(TokenType.comma)) {
operator = [TokenType.comma];
} else {
operator = _parseJoinOperatorNoComma();
}
}
return JoinClause(primary: start, joins: joins);
}
/// Parses https://www.sqlite.org/syntax/join-operator.html, minus the comma.
List<TokenType> _parseJoinOperatorNoComma() {
if (_match(_startOperators)) {
final operators = [_previous.type];
if (_previous.type == TokenType.join) {
// just join, without any specific operators
return operators;
} else {
// natural is a prefix, another operator can follow.
if (_previous.type == TokenType.natural) {
if (_match([TokenType.left, TokenType.inner, TokenType.cross])) {
operators.add(_previous.type);
}
}
if (_previous.type == TokenType.left && _matchOne(TokenType.outer)) {
operators.add(_previous.type);
}
_consume(TokenType.join, 'Expected to see a join keyword here');
return operators;
}
}
return null;
}
/// Parses https://www.sqlite.org/syntax/join-constraint.html
JoinConstraint _joinConstraint() {
if (_matchOne(TokenType.on)) {
return OnConstraint(expression: expression());
} else if (_matchOne(TokenType.using)) {
_consume(TokenType.leftParen, 'Expected an opening paranthesis');
final columnNames = <String>[];
do {
final identifier =
_consume(TokenType.identifier, 'Expected a column name');
columnNames.add((identifier as IdentifierToken).identifier);
} while (_matchOne(TokenType.comma));
_consume(TokenType.rightParen, 'Expected an closing paranthesis');
return UsingConstraint(columnNames: columnNames);
}
_error('Expected a constraint with ON or USING');
}
/// Parses a where clause if there is one at the current position
Expression _where() {
if (_match(const [TokenType.where])) {
return expression();
}
return null;
}
GroupBy _groupBy() {
if (_matchOne(TokenType.group)) {
_consume(TokenType.by, 'Expected a "BY"');
final by = <Expression>[];
Expression having;
do {
by.add(expression());
} while (_matchOne(TokenType.comma));
if (_matchOne(TokenType.having)) {
having = expression();
}
return GroupBy(by: by, having: having);
}
return null;
}
OrderBy _orderBy() {
if (_match(const [TokenType.order])) {
_consume(TokenType.by, 'Expected "BY" after "ORDER" token');
final terms = <OrderingTerm>[];
do {
terms.add(_orderingTerm());
} while (_matchOne(TokenType.comma));
return OrderBy(terms: terms);
}
return null;
}
OrderingTerm _orderingTerm() {
final expr = expression();
return OrderingTerm(expression: expr, orderingMode: _orderingModeOrNull());
}
OrderingMode _orderingModeOrNull() {
if (_match(const [TokenType.asc, TokenType.desc])) {
final mode = _previous.type == TokenType.asc
? OrderingMode.ascending
: OrderingMode.descending;
return mode;
}
return null;
}
/// Parses a [Limit] clause, or returns null if there is no limit token after
/// the current position.
Limit _limit() {
if (!_matchOne(TokenType.limit)) return null;
// Unintuitive, it's "$amount OFFSET $offset", but "$offset, $amount"
// the order changes between the separator tokens.
final first = expression();
if (_matchOne(TokenType.comma)) {
final separator = _previous;
final count = expression();
return Limit(count: count, offsetSeparator: separator, offset: first);
} else if (_matchOne(TokenType.offset)) {
final separator = _previous;
final offset = expression();
return Limit(count: first, offsetSeparator: separator, offset: offset);
} else {
return Limit(count: first);
}
}
DeleteStatement _deleteStmt() {
if (!_matchOne(TokenType.delete)) return null;
_consume(TokenType.from, 'Expected a FROM here');
final table = _tableReference();
Expression where;
if (table == null) {
_error('Expected a table reference');
}
if (_matchOne(TokenType.where)) {
where = expression();
}
return DeleteStatement(from: table, where: where);
}
UpdateStatement _update() {
if (!_matchOne(TokenType.update)) return null;
FailureMode failureMode;
if (_matchOne(TokenType.or)) {
failureMode = UpdateStatement.failureModeFromToken(_advance().type);
}
final table = _tableReference();
_consume(TokenType.set, 'Expected SET after the table name');
final set = <SetComponent>[];
do {
final reference = _primary() as Reference;
_consume(TokenType.equal, 'Expected = after the column name');
final expr = expression();
set.add(SetComponent(column: reference, expression: expr));
} while (_matchOne(TokenType.comma));
final where = _where();
return UpdateStatement(
or: failureMode, table: table, set: set, where: where);
}
CreateTableStatement _createTable() {
if (!_matchOne(TokenType.create)) return null;
final first = _previous;
_consume(TokenType.table, 'Expected TABLE keyword here');
var ifNotExists = false;
if (_matchOne(TokenType.$if)) {
_consume(TokenType.not, 'Expected IF to be followed by NOT EXISTS');
_consume(TokenType.exists, 'Expected IF NOT to be followed by EXISTS');
ifNotExists = true;
}
final tableIdentifier =
_consume(TokenType.identifier, 'Expected a table name')
as IdentifierToken;
// we don't currently support CREATE TABLE x AS SELECT ... statements
_consume(
TokenType.leftParen, 'Expected opening parenthesis to list columns');
final columns = <ColumnDefinition>[];
do {
columns.add(_columnDefinition());
} while (_matchOne(TokenType.comma));
// todo parse table constraints
_consume(TokenType.rightParen, 'Expected closing parenthesis');
var withoutRowId = false;
if (_matchOne(TokenType.without)) {
_consume(
TokenType.rowid, 'Expected ROWID to complete the WITHOUT ROWID part');
withoutRowId = true;
}
return CreateTableStatement(
ifNotExists: ifNotExists,
tableName: tableIdentifier.identifier,
withoutRowId: withoutRowId,
columns: columns,
)..setSpan(first, _previous);
}
ColumnDefinition _columnDefinition() {
final name = _consume(TokenType.identifier, 'Expected a column name')
as IdentifierToken;
IdentifierToken typeName;
if (_matchOne(TokenType.identifier)) {
typeName = _previous as IdentifierToken;
}
final constraints = <ColumnConstraint>[];
ColumnConstraint constraint;
while ((constraint = _columnConstraint(orNull: true)) != null) {
constraints.add(constraint);
}
return ColumnDefinition(
columnName: name.identifier,
typeName: typeName?.identifier,
constraints: constraints,
)..setSpan(name, _previous);
}
ColumnConstraint _columnConstraint({bool orNull = false}) {
Token first;
IdentifierToken name;
if (_matchOne(TokenType.constraint)) {
first = _previous;
name = _consume(
TokenType.identifier, 'Expect a name for the constraint here')
as IdentifierToken;
}
final resolvedName = name?.identifier;
if (_matchOne(TokenType.primary)) {
// set reference to first token in this constraint if not set because of
// the CONSTRAINT token
first ??= _previous;
_consume(TokenType.key, 'Expected KEY to complete PRIMARY KEY clause');
final mode = _orderingModeOrNull();
final conflict = _conflictClauseOrNull();
final hasAutoInc = _matchOne(TokenType.autoincrement);
return PrimaryKey(resolvedName,
autoIncrement: hasAutoInc, mode: mode, onConflict: conflict)
..setSpan(first, _previous);
}
if (_matchOne(TokenType.not)) {
first ??= _previous;
_consume(TokenType.$null, 'Expected NULL to complete NOT NULL');
return NotNull(resolvedName, onConflict: _conflictClauseOrNull())
..setSpan(first, _previous);
}
if (_matchOne(TokenType.unique)) {
first ??= _previous;
return Unique(resolvedName, _conflictClauseOrNull())
..setSpan(first, _previous);
}
if (_matchOne(TokenType.check)) {
first ??= _previous;
_consume(TokenType.leftParen, 'Expected opening parenthesis');
final expr = expression();
_consume(TokenType.rightParen, 'Expected closing parenthesis');
return Check(resolvedName, expr)..setSpan(first, _previous);
}
if (_matchOne(TokenType.$default)) {
first ??= _previous;
Expression expr = _literalOrNull();
if (expr == null) {
// no literal, expect (expression)
_consume(TokenType.leftParen,
'Expected opening parenthesis before expression');
expr = expression();
_consume(TokenType.rightParen, 'Expected closing parenthesis');
}
return Default(resolvedName, expr);
}
if (_matchOne(TokenType.collate)) {
first ??= _previous;
final collation =
_consume(TokenType.identifier, 'Expected the collation name')
as IdentifierToken;
return CollateConstraint(resolvedName, collation.identifier)
..setSpan(first, _previous);
}
// todo foreign key clauses
// no known column constraint matched. If orNull is set and we're not
// guaranteed to be in a constraint clause (started with CONSTRAINT), we
// can return null
if (orNull && name == null) {
return null;
}
_error('Expected a constraint (primary key, nullability, etc.)');
}
ConflictClause _conflictClauseOrNull() {
if (_matchOne(TokenType.on)) {
_consume(TokenType.conflict,
'Expected CONFLICT to complete ON CONFLICT clause');
const modes = {
TokenType.rollback: ConflictClause.rollback,
TokenType.abort: ConflictClause.abort,
TokenType.fail: ConflictClause.fail,
TokenType.ignore: ConflictClause.ignore,
TokenType.replace: ConflictClause.replace,
};
if (_match(modes.keys)) {
return modes[_previous.type];
} else {
_error('Expected a conflict handler (rollback, abort, etc.) here');
}
}
return null;
}
/* We parse expressions here.
* Operators have the following precedence:
* - + ~ NOT (unary)
* || (concatenation)
* * / %
* + -
* << >> & |
* < <= > >=
* = == != <> IS IS NOT IN LIKE GLOB MATCH REGEXP
* AND
* OR
* We also treat expressions in parentheses and literals with the highest
* priority. Parsing methods are written in ascending precedence, and each
* parsing method calls the next higher precedence if unsuccessful.
* https://www.sqlite.org/lang_expr.html
* */
Expression expression() {
return _case();
}
Expression _case() {
if (_matchOne(TokenType.$case)) {
final base = _check(TokenType.when) ? null : _or();
final whens = <WhenComponent>[];
Expression $else;
while (_matchOne(TokenType.when)) {
final whenExpr = _or();
_consume(TokenType.then, 'Expected THEN');
final then = _or();
whens.add(WhenComponent(when: whenExpr, then: then));
}
if (_matchOne(TokenType.$else)) {
$else = _or();
}
_consume(TokenType.end, 'Expected END to finish the case operator');
return CaseExpression(whens: whens, base: base, elseExpr: $else);
}
return _or();
}
/// Parses an expression of the form a <T> b, where <T> is in [types] and
/// both a and b are expressions with a higher precedence parsed from
/// [higherPrecedence].
Expression _parseSimpleBinary(
List<TokenType> types, Expression Function() higherPrecedence) {
var expression = higherPrecedence();
while (_match(types)) {
final operator = _previous;
final right = higherPrecedence();
expression = BinaryExpression(expression, operator, right);
}
return expression;
}
Expression _or() => _parseSimpleBinary(const [TokenType.or], _and);
Expression _and() => _parseSimpleBinary(const [TokenType.and], _in);
Expression _in() {
final left = _equals();
if (_checkWithNot(TokenType.$in)) {
final not = _matchOne(TokenType.not);
_matchOne(TokenType.$in);
var inside = _equals();
if (inside is Parentheses) {
// if we have something like x IN (3), then (3) is a tuple and not a
// parenthesis. We can only know this from the context unfortunately
inside = (inside as Parentheses).asTuple;
}
return InExpression(left: left, inside: inside, not: not);
}
return left;
}
/// Parses expressions with the "equals" precedence. This contains
/// comparisons, "IS (NOT) IN" expressions, between expressions and "like"
/// expressions.
Expression _equals() {
var expression = _comparison();
final ops = const [
TokenType.equal,
TokenType.doubleEqual,
TokenType.exclamationEqual,
TokenType.lessMore,
TokenType.$is,
];
final stringOps = const [
TokenType.like,
TokenType.glob,
TokenType.match,
TokenType.regexp,
];
while (true) {
if (_checkWithNot(TokenType.between)) {
final not = _matchOne(TokenType.not);
_consume(TokenType.between, 'expected a BETWEEN');
final lower = _comparison();
_consume(TokenType.and, 'expected AND');
final upper = _comparison();
expression = BetweenExpression(
not: not, check: expression, lower: lower, upper: upper);
} else if (_match(ops)) {
final operator = _previous;
if (operator.type == TokenType.$is) {
final not = _match(const [TokenType.not]);
// special case: is not expression
expression = IsExpression(not, expression, _comparison());
} else {
expression = BinaryExpression(expression, operator, _comparison());
}
} else if (_checkAnyWithNot(stringOps)) {
final not = _matchOne(TokenType.not);
_match(stringOps); // will consume, existence was verified with check
final operator = _previous;
final right = _comparison();
Expression escape;
if (_matchOne(TokenType.escape)) {
escape = _comparison();
}
expression = StringComparisonExpression(
not: not,
left: expression,
operator: operator,
right: right,
escape: escape);
} else {
break; // no matching operator with this precedence was found
}
}
return expression;
}
Expression _comparison() {
return _parseSimpleBinary(_comparisonOperators, _binaryOperation);
}
Expression _binaryOperation() {
return _parseSimpleBinary(_binaryOperators, _addition);
}
Expression _addition() {
return _parseSimpleBinary(const [
TokenType.plus,
TokenType.minus,
], _multiplication);
}
Expression _multiplication() {
return _parseSimpleBinary(const [
TokenType.star,
TokenType.slash,
TokenType.percent,
], _concatenation);
}
Expression _concatenation() {
return _parseSimpleBinary(const [TokenType.doublePipe], _unary);
}
Expression _unary() {
if (_match(const [
TokenType.minus,
TokenType.plus,
TokenType.tilde,
TokenType.not,
])) {
final operator = _previous;
final expression = _unary();
return UnaryExpression(operator, expression);
} else if (_matchOne(TokenType.exists)) {
_consume(
TokenType.leftParen, 'Expected opening parenthesis after EXISTS');
final selectStmt = select();
_consume(TokenType.rightParen,
'Expected closing paranthesis to finish EXISTS expression');
return ExistsExpression(select: selectStmt);
}
return _postfix();
}
Expression _postfix() {
// todo parse ISNULL, NOTNULL, NOT NULL, etc.
// I don't even know the precedence ¯\_(ツ)_/¯ (probably not higher than
// unary)
var expression = _primary();
while (_matchOne(TokenType.collate)) {
final collateOp = _previous;
final collateFun =
_consume(TokenType.identifier, 'Expected a collating sequence')
as IdentifierToken;
expression = CollateExpression(
inner: expression,
operator: collateOp,
collateFunction: collateFun,
)..setSpan(expression.first, collateFun);
}
return expression;
}
Literal _literalOrNull() {
final token = _peek;
Literal _parseInner() {
if (_matchOne(TokenType.numberLiteral)) {
return NumericLiteral(_parseNumber(token.lexeme), token);
}
if (_matchOne(TokenType.stringLiteral)) {
return StringLiteral(token as StringLiteralToken);
}
if (_matchOne(TokenType.$null)) {
return NullLiteral(token);
}
if (_matchOne(TokenType.$true)) {
return BooleanLiteral.withTrue(token);
}
if (_matchOne(TokenType.$false)) {
return BooleanLiteral.withFalse(token);
}
// todo CURRENT_TIME, CURRENT_DATE, CURRENT_TIMESTAMP
return null;
}
final literal = _parseInner();
literal?.setSpan(token, token);
return literal;
}
Expression _primary() {
final literal = _literalOrNull();
if (literal != null) return literal;
final token = _advance();
final type = token.type;
switch (type) {
case TokenType.leftParen:
// Opening brackets could be three things: An inner select statement
// (SELECT ...), a parenthesised expression, or a tuple of expressions
// (a, b, c).
final left = token;
if (_peek.type == TokenType.select) {
final stmt = select();
_consume(TokenType.rightParen, 'Expected a closing bracket');
return SubQuery(select: stmt);
} else {
// alright, it's either a tuple or just parenthesis. A tuple can be
// empty, so if the next statement is the closing bracket we're done
if (_matchOne(TokenType.rightParen)) {
return TupleExpression(expressions: [])..setSpan(left, _previous);
}
final expr = expression();
// Are we witnessing a tuple?
if (_check(TokenType.comma)) {
// we are, add expressions as long as we see commas
final exprs = [expr];
while (_matchOne(TokenType.comma)) {
exprs.add(expression());
}
_consume(TokenType.rightParen, 'Expected a closing bracket');
return TupleExpression(expressions: exprs);
} else {
// we aren't, so that'll just be parentheses.
_consume(TokenType.rightParen, 'Expected a closing bracket');
return Parentheses(left, expr, token);
}
}
break;
case TokenType.identifier:
// could be table.column, function(...) or just column
final first = token as IdentifierToken;
if (_matchOne(TokenType.dot)) {
final second =
_consume(TokenType.identifier, 'Expected a column name here')
as IdentifierToken;
return Reference(
tableName: first.identifier, columnName: second.identifier)
..setSpan(first, second);
} else if (_matchOne(TokenType.leftParen)) {
final parameters = _functionParameters();
final rightParen = _consume(TokenType.rightParen,
'Expected closing bracket after argument list');
return FunctionExpression(
name: first.identifier, parameters: parameters)
..setSpan(first, rightParen);
} else {
return Reference(columnName: first.identifier)..setSpan(first, first);
}
break;
case TokenType.questionMark:
final mark = token;
if (_matchOne(TokenType.numberLiteral)) {
final number = _previous;
return NumberedVariable(mark, _parseNumber(number.lexeme).toInt())
..setSpan(mark, number);
} else {
return NumberedVariable(mark, null)..setSpan(mark, mark);
}
break;
case TokenType.colon:
final colon = token;
final identifier = _consume(TokenType.identifier,
'Expected an identifier for the named variable') as IdentifierToken;
final content = identifier.identifier;
return ColonNamedVariable(':$content')..setSpan(colon, identifier);
default:
break;
}
// nothing found -> issue error
_error('Could not parse this expression');
}
FunctionParameters _functionParameters() {
if (_matchOne(TokenType.star)) {
return const StarFunctionParameter();
}
final distinct = _matchOne(TokenType.distinct);
final parameters = <Expression>[];
while (_peek.type != TokenType.rightParen) {
parameters.add(expression());
}
return ExprFunctionParameters(distinct: distinct, parameters: parameters);
}
}

View File

@ -0,0 +1,174 @@
part of 'parser.dart';
mixin SchemaParser on ParserBase {
CreateTableStatement _createTable() {
if (!_matchOne(TokenType.create)) return null;
final first = _previous;
_consume(TokenType.table, 'Expected TABLE keyword here');
var ifNotExists = false;
if (_matchOne(TokenType.$if)) {
_consume(TokenType.not, 'Expected IF to be followed by NOT EXISTS');
_consume(TokenType.exists, 'Expected IF NOT to be followed by EXISTS');
ifNotExists = true;
}
final tableIdentifier =
_consume(TokenType.identifier, 'Expected a table name')
as IdentifierToken;
// we don't currently support CREATE TABLE x AS SELECT ... statements
_consume(
TokenType.leftParen, 'Expected opening parenthesis to list columns');
final columns = <ColumnDefinition>[];
do {
columns.add(_columnDefinition());
} while (_matchOne(TokenType.comma));
// todo parse table constraints
_consume(TokenType.rightParen, 'Expected closing parenthesis');
var withoutRowId = false;
if (_matchOne(TokenType.without)) {
_consume(
TokenType.rowid, 'Expected ROWID to complete the WITHOUT ROWID part');
withoutRowId = true;
}
return CreateTableStatement(
ifNotExists: ifNotExists,
tableName: tableIdentifier.identifier,
withoutRowId: withoutRowId,
columns: columns,
)..setSpan(first, _previous);
}
ColumnDefinition _columnDefinition() {
final name = _consume(TokenType.identifier, 'Expected a column name')
as IdentifierToken;
IdentifierToken typeName;
if (_matchOne(TokenType.identifier)) {
typeName = _previous as IdentifierToken;
}
final constraints = <ColumnConstraint>[];
ColumnConstraint constraint;
while ((constraint = _columnConstraint(orNull: true)) != null) {
constraints.add(constraint);
}
return ColumnDefinition(
columnName: name.identifier,
typeName: typeName?.identifier,
constraints: constraints,
)..setSpan(name, _previous);
}
ColumnConstraint _columnConstraint({bool orNull = false}) {
Token first;
IdentifierToken name;
if (_matchOne(TokenType.constraint)) {
first = _previous;
name = _consume(
TokenType.identifier, 'Expect a name for the constraint here')
as IdentifierToken;
}
final resolvedName = name?.identifier;
if (_matchOne(TokenType.primary)) {
// set reference to first token in this constraint if not set because of
// the CONSTRAINT token
first ??= _previous;
_consume(TokenType.key, 'Expected KEY to complete PRIMARY KEY clause');
final mode = _orderingModeOrNull();
final conflict = _conflictClauseOrNull();
final hasAutoInc = _matchOne(TokenType.autoincrement);
return PrimaryKey(resolvedName,
autoIncrement: hasAutoInc, mode: mode, onConflict: conflict)
..setSpan(first, _previous);
}
if (_matchOne(TokenType.not)) {
first ??= _previous;
_consume(TokenType.$null, 'Expected NULL to complete NOT NULL');
return NotNull(resolvedName, onConflict: _conflictClauseOrNull())
..setSpan(first, _previous);
}
if (_matchOne(TokenType.unique)) {
first ??= _previous;
return Unique(resolvedName, _conflictClauseOrNull())
..setSpan(first, _previous);
}
if (_matchOne(TokenType.check)) {
first ??= _previous;
_consume(TokenType.leftParen, 'Expected opening parenthesis');
final expr = expression();
_consume(TokenType.rightParen, 'Expected closing parenthesis');
return Check(resolvedName, expr)..setSpan(first, _previous);
}
if (_matchOne(TokenType.$default)) {
first ??= _previous;
Expression expr = _literalOrNull();
if (expr == null) {
// no literal, expect (expression)
_consume(TokenType.leftParen,
'Expected opening parenthesis before expression');
expr = expression();
_consume(TokenType.rightParen, 'Expected closing parenthesis');
}
return Default(resolvedName, expr);
}
if (_matchOne(TokenType.collate)) {
first ??= _previous;
final collation =
_consume(TokenType.identifier, 'Expected the collation name')
as IdentifierToken;
return CollateConstraint(resolvedName, collation.identifier)
..setSpan(first, _previous);
}
// todo foreign key clauses
// no known column constraint matched. If orNull is set and we're not
// guaranteed to be in a constraint clause (started with CONSTRAINT), we
// can return null
if (orNull && name == null) {
return null;
}
_error('Expected a constraint (primary key, nullability, etc.)');
}
ConflictClause _conflictClauseOrNull() {
if (_matchOne(TokenType.on)) {
_consume(TokenType.conflict,
'Expected CONFLICT to complete ON CONFLICT clause');
const modes = {
TokenType.rollback: ConflictClause.rollback,
TokenType.abort: ConflictClause.abort,
TokenType.fail: ConflictClause.fail,
TokenType.ignore: ConflictClause.ignore,
TokenType.replace: ConflictClause.replace,
};
if (_match(modes.keys)) {
return modes[_previous.type];
} else {
_error('Expected a conflict handler (rollback, abort, etc.) here');
}
}
return null;
}
}