diff --git a/sqlparser/CHANGELOG.md b/sqlparser/CHANGELOG.md index 9e014b16..433bf589 100644 --- a/sqlparser/CHANGELOG.md +++ b/sqlparser/CHANGELOG.md @@ -2,7 +2,7 @@ - Added a argument type and argument to the visitor classes - Experimental new type inference algorithm -- Support `CAST` expressions. +- Support `CAST` expressions and the `ISNULL` / `NOTNULL` postfixes - Support parsing `CREATE TRIGGER` statements - Support parsing `CREATE INDEX` statement diff --git a/sqlparser/lib/src/analysis/types/resolver.dart b/sqlparser/lib/src/analysis/types/resolver.dart index 85cb82aa..58871b94 100644 --- a/sqlparser/lib/src/analysis/types/resolver.dart +++ b/sqlparser/lib/src/analysis/types/resolver.dart @@ -115,6 +115,7 @@ class TypeResolver { } else if (expr is SqlInvocation) { return resolveFunctionCall(expr); } else if (expr is IsExpression || + expr is IsNullExpression || expr is InExpression || expr is StringComparisonExpression || expr is BetweenExpression || diff --git a/sqlparser/lib/src/ast/expressions/simple.dart b/sqlparser/lib/src/ast/expressions/simple.dart index 1c140426..adc5aa68 100644 --- a/sqlparser/lib/src/ast/expressions/simple.dart +++ b/sqlparser/lib/src/ast/expressions/simple.dart @@ -105,6 +105,28 @@ class IsExpression extends Expression { } } +class IsNullExpression extends Expression { + final Expression operand; + + /// When true, this is a `NOT NULL` expression. + final bool negated; + + IsNullExpression(this.operand, [this.negated = false]); + + @override + R accept(AstVisitor visitor, A arg) { + return visitor.visitIsNullExpression(this, arg); + } + + @override + Iterable get childNodes => [operand]; + + @override + bool contentEquals(IsNullExpression other) { + return other.negated == negated; + } +} + /// `$check BETWEEN $lower AND $upper` class BetweenExpression extends Expression { final bool not; diff --git a/sqlparser/lib/src/ast/visitor.dart b/sqlparser/lib/src/ast/visitor.dart index 61d9dd43..04f344b5 100644 --- a/sqlparser/lib/src/ast/visitor.dart +++ b/sqlparser/lib/src/ast/visitor.dart @@ -34,6 +34,7 @@ abstract class AstVisitor { R visitStringComparison(StringComparisonExpression e, A arg); R visitUnaryExpression(UnaryExpression e, A arg); R visitIsExpression(IsExpression e, A arg); + R visitIsNullExpression(IsNullExpression e, A arg); R visitBetweenExpression(BetweenExpression e, A arg); R visitLiteral(Literal e, A arg); R visitReference(Reference e, A arg); @@ -281,6 +282,11 @@ class RecursiveVisitor implements AstVisitor { return visitExpression(e, arg); } + @override + R visitIsNullExpression(IsNullExpression e, A arg) { + return visitExpression(e, arg); + } + @override R visitBetweenExpression(BetweenExpression e, A arg) { return visitExpression(e, arg); diff --git a/sqlparser/lib/src/reader/parser/expressions.dart b/sqlparser/lib/src/reader/parser/expressions.dart index 815c33ed..77209505 100644 --- a/sqlparser/lib/src/reader/parser/expressions.dart +++ b/sqlparser/lib/src/reader/parser/expressions.dart @@ -205,21 +205,41 @@ mixin ExpressionParser on ParserBase { } 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); + // todo we don't currently parse "NOT NULL" (2 tokens) because of ambiguity + // with NOT BETWEEN / NOT IN / ... expressions + const matchedTokens = [ + TokenType.collate, + TokenType.notNull, + TokenType.isNull + ]; + + while (_match(matchedTokens)) { + final operator = _previous; + switch (operator.type) { + case TokenType.collate: + final collateOp = _previous; + final collateFun = + _consume(TokenType.identifier, 'Expected a collating sequence') + as IdentifierToken; + expression = CollateExpression( + inner: expression, + operator: collateOp, + collateFunction: collateFun, + ); + break; + case TokenType.isNull: + expression = IsNullExpression(expression); + break; + case TokenType.notNull: + expression = IsNullExpression(expression, true); + break; + default: + // we checked with _match, this may never happen + throw AssertionError(); + } + expression.setSpan(operator, _previous); } return expression; diff --git a/sqlparser/lib/src/reader/tokenizer/token.dart b/sqlparser/lib/src/reader/tokenizer/token.dart index 93d3a551..4ad8a912 100644 --- a/sqlparser/lib/src/reader/tokenizer/token.dart +++ b/sqlparser/lib/src/reader/tokenizer/token.dart @@ -50,6 +50,8 @@ enum TokenType { $true, $false, $null, + isNull, + notNull, currentTime, currentDate, currentTimestamp, @@ -207,6 +209,8 @@ const Map keywords = { 'TRUE': TokenType.$true, 'FALSE': TokenType.$false, 'NULL': TokenType.$null, + 'ISNULL': TokenType.isNull, + 'NOTNULL': TokenType.notNull, 'CURRENT_TIME': TokenType.currentTime, 'CURRENT_DATE': TokenType.currentDate, 'CURRENT_TIMESTAMP': TokenType.currentTimestamp, diff --git a/sqlparser/test/parser/expression_test.dart b/sqlparser/test/parser/expression_test.dart index 7fe2fdd8..2d3bc808 100644 --- a/sqlparser/test/parser/expression_test.dart +++ b/sqlparser/test/parser/expression_test.dart @@ -144,6 +144,8 @@ final Map _testCases = { ), 'TEXT', ), + 'foo ISNULL': IsNullExpression(Reference(columnName: 'foo')), + 'foo NOTNULL': IsNullExpression(Reference(columnName: 'foo'), true), }; void main() {