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() {