From 7b6802e1c50f1568be896e5dc0c80af5ae04dbec Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Sat, 27 Jul 2019 20:47:11 +0200 Subject: [PATCH] Split parser implementation into multiple files --- sqlparser/lib/src/reader/parser/crud.dart | 351 +++++++ .../lib/src/reader/parser/expressions.dart | 349 +++++++ sqlparser/lib/src/reader/parser/parser.dart | 898 +----------------- sqlparser/lib/src/reader/parser/schema.dart | 174 ++++ 4 files changed, 900 insertions(+), 872 deletions(-) create mode 100644 sqlparser/lib/src/reader/parser/crud.dart create mode 100644 sqlparser/lib/src/reader/parser/expressions.dart create mode 100644 sqlparser/lib/src/reader/parser/schema.dart diff --git a/sqlparser/lib/src/reader/parser/crud.dart b/sqlparser/lib/src/reader/parser/crud.dart new file mode 100644 index 00000000..dc3664ee --- /dev/null +++ b/sqlparser/lib/src/reader/parser/crud.dart @@ -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 = []; + 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 _from() { + if (!_matchOne(TokenType.from)) return []; + + // Can either be a list of 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 = []; + + 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 _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 = []; + 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 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 = []; + 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 = []; + 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); + } +} diff --git a/sqlparser/lib/src/reader/parser/expressions.dart b/sqlparser/lib/src/reader/parser/expressions.dart new file mode 100644 index 00000000..ca4f2691 --- /dev/null +++ b/sqlparser/lib/src/reader/parser/expressions.dart @@ -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 = []; + 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 b, where is in [types] and + /// both a and b are expressions with a higher precedence parsed from + /// [higherPrecedence]. + Expression _parseSimpleBinary( + List 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 = []; + while (_peek.type != TokenType.rightParen) { + parameters.add(expression()); + } + return ExprFunctionParameters(distinct: distinct, parameters: parameters); + } +} diff --git a/sqlparser/lib/src/reader/parser/parser.dart b/sqlparser/lib/src/reader/parser/parser.dart index 23a3d2e0..78e3a07e 100644 --- a/sqlparser/lib/src/reader/parser/parser.dart +++ b/sqlparser/lib/src/reader/parser/parser.dart @@ -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 tokens; final List 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 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 = []; - 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 _from() { - if (!_matchOne(TokenType.from)) return []; - - // Can either be a list of 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 = []; - - 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 _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 = []; - 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 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 = []; - 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 = []; - 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 = []; - 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 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 = []; - 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 b, where is in [types] and - /// both a and b are expressions with a higher precedence parsed from - /// [higherPrecedence]. - Expression _parseSimpleBinary( - List 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 = []; - while (_peek.type != TokenType.rightParen) { - parameters.add(expression()); - } - return ExprFunctionParameters(distinct: distinct, parameters: parameters); - } } diff --git a/sqlparser/lib/src/reader/parser/schema.dart b/sqlparser/lib/src/reader/parser/schema.dart new file mode 100644 index 00000000..3af26256 --- /dev/null +++ b/sqlparser/lib/src/reader/parser/schema.dart @@ -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 = []; + 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 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; + } +}