Repository: tobymao/sqlglot

Issue:
Invalid expression on using NOTNULL
I have a sql query that has looks like below:

```
query = """
SELECT datasets.a,
        id as history_id,
        app_users.id as user_id,
        companies.name AS companyname,
        app_users.email as email,
        app_users.group_id as group_id,
    
    FROM datasets
    JOIN companies ON datasets.companyid = companies.companyid
    JOIN app_users USING (short_user_id)

    WHERE (datasets.companyid = '405')
    AND (mmh_trip_start NOTNULL)
    
"""
```

when I try to parse it  using `parse_one(query)` command I get the following error:

```
 raise ParseError(concat_errors(self.errors, self.max_errors))
sqlglot.errors.ParseError: Expecting ). Line 14, Col: 25.
  OIN app_users USING (short_user_id)

    WHERE (datasets.companyid = '405')
    AND (mmh_trip_start NOTNULL)
    


Invalid expression / Unexpected token. Line 14, Col: 25.
  OIN app_users USING (short_user_id)

    WHERE (datasets.companyid = '405')
    AND (mmh_trip_start NOTNULL)

```

However the parsing goes through without error when I change the query to use ` IS NOT NULL` instead of `NOTNULL` . Reading through the issues section related to parse errors, I can see that at times support for certain keywords were missing and added later. Just wanted to check if `NOTNULL` is supported ?

P.S : This query was generated for postgres db and runs perfectly fine on it.

[Here is a link to usage of `NOTNULL` keyword  along with comparison with few others](https://blog.andreiavram.ro/postgresql-null-composite-type/)


Discussion:
Thanks for the report, I'll take a look at it. Indeed, `NOTNULL` is not currently supported.

Resolving patch:
diff --git a/sqlglot/parser.py b/sqlglot/parser.py
--- a/sqlglot/parser.py
+++ b/sqlglot/parser.py
@@ -1536,6 +1536,14 @@ def _parse_range(self):
 
         if self._match_set(self.RANGE_PARSERS):
             this = self.RANGE_PARSERS[self._prev.token_type](self, this)
+        elif self._match(TokenType.ISNULL):
+            this = self.expression(exp.Is, this=this, expression=exp.Null())
+
+        # Postgres supports ISNULL and NOTNULL for conditions.
+        # Source: https://blog.andreiavram.ro/postgresql-null-composite-type/
+        if self._match(TokenType.NOTNULL):
+            this = self.expression(exp.Is, this=this, expression=exp.Null())
+            this = self.expression(exp.Not, this=this)
 
         if negate:
             this = self.expression(exp.Not, this=this)
diff --git a/sqlglot/tokens.py b/sqlglot/tokens.py
--- a/sqlglot/tokens.py
+++ b/sqlglot/tokens.py
@@ -199,6 +199,7 @@ class TokenType(AutoName):
     NATURAL = auto()
     NEXT = auto()
     NO_ACTION = auto()
+    NOTNULL = auto()
     NULL = auto()
     NULLS_FIRST = auto()
     NULLS_LAST = auto()
@@ -498,6 +499,7 @@ class Tokenizer(metaclass=_Tokenizer):
         "NEXT": TokenType.NEXT,
         "NO ACTION": TokenType.NO_ACTION,
         "NOT": TokenType.NOT,
+        "NOTNULL": TokenType.NOTNULL,
         "NULL": TokenType.NULL,
         "NULLS FIRST": TokenType.NULLS_FIRST,
         "NULLS LAST": TokenType.NULLS_LAST,


Test patch:
diff --git a/tests/dialects/test_postgres.py b/tests/dialects/test_postgres.py
--- a/tests/dialects/test_postgres.py
+++ b/tests/dialects/test_postgres.py
@@ -68,6 +68,7 @@ def test_postgres(self):
         self.validate_identity("SELECT TRIM(' X' FROM ' XXX ')")
         self.validate_identity("SELECT TRIM(LEADING 'bla' FROM ' XXX ' COLLATE utf8_bin)")
         self.validate_identity("SELECT TO_TIMESTAMP(1284352323.5), TO_TIMESTAMP('05 Dec 2000', 'DD Mon YYYY')")
+        self.validate_identity("COMMENT ON TABLE mytable IS 'this'")
 
         self.validate_all(
             "CREATE TABLE x (a UUID, b BYTEA)",
@@ -163,4 +164,11 @@ def test_postgres(self):
                 "postgres": "SELECT p1.id, p2.id, v1, v2 FROM polygons p1, polygons p2, LATERAL VERTICES(p1.poly) v1, LATERAL VERTICES(p2.poly) v2 WHERE (v1 <-> v2) < 10 AND p1.id != p2.id",
             },
         )
-        self.validate_identity("COMMENT ON TABLE mytable IS 'this'")
+        self.validate_all(
+            "SELECT id, email, CAST(deleted AS TEXT) FROM users WHERE NOT deleted IS NULL",
+            read={"postgres": "SELECT id, email, CAST(deleted AS TEXT) FROM users WHERE deleted NOTNULL"},
+        )
+        self.validate_all(
+            "SELECT id, email, CAST(deleted AS TEXT) FROM users WHERE NOT deleted IS NULL",
+            read={"postgres": "SELECT id, email, CAST(deleted AS TEXT) FROM users WHERE NOT deleted ISNULL"},
+        )