Repository: tobymao/sqlglot

Issue:
Issue with Parsing IS JSON Function in PostgreSQL Dialect
I'm encountering an issue with the PostgreSQL dialect in SQLGlot where it fails to parse a query containing the IS JSON function. The expected behavior is to convert the PostgreSQL query to an AST, but instead, it throws a ParseError. 

**Below is the code snippet that reproduces the issue:**

```
from sqlglot.dialects.postgres import Postgres
from sqlglot import parse_one

sql = """
SELECT js,
  js IS JSON "json?",
  js IS JSON SCALAR "scalar?",
  js IS JSON OBJECT "object?",
  js IS JSON ARRAY "array?"
FROM (VALUES
      ('123'), ('"abc"'), ('{"a": "b"}'), ('[1,2]'),('abc')) foo(js);
"""

ast = parse_one(sql, dialect=Postgres)
print(repr(ast))
```

**ERROR :** 

`sqlglot.errors.ParseError: Invalid expression / Unexpected token. Line 3, Col: 11.`

It seems that the parser does not handle the IS JSON syntax correctly, particularly with the modifiers like SCALAR, OBJECT, and ARRAY.

**Expected Behavior:**

The query should be parsed into an AST without errors.

**Official Documentation**
[https://www.postgresql.org/docs/current/functions-json.html](https://www.postgresql.org/docs/current/functions-json.html)


Discussion:

Patch:
diff --git a/sqlglot/expressions.py b/sqlglot/expressions.py
index d43c2e22..7cc4e656 100644
--- a/sqlglot/expressions.py
+++ b/sqlglot/expressions.py
@@ -5441,6 +5441,11 @@ class IsInf(Func):
     _sql_names = ["IS_INF", "ISINF"]
 
 
+# https://www.postgresql.org/docs/current/functions-json.html
+class JSON(Expression):
+    arg_types = {"this": False, "with": False, "unique": False}
+
+
 class JSONPath(Expression):
     arg_types = {"expressions": True}
 
diff --git a/sqlglot/generator.py b/sqlglot/generator.py
index 7796d239..97aaf4b0 100644
--- a/sqlglot/generator.py
+++ b/sqlglot/generator.py
@@ -4136,3 +4136,20 @@ class Generator(metaclass=_Generator):
         expr = exp.AtTimeZone(this=timestamp, zone=target_tz)
 
         return self.sql(expr)
+
+    def json_sql(self, expression: exp.JSON) -> str:
+        this = self.sql(expression, "this")
+        this = f" {this}" if this else ""
+
+        _with = expression.args.get("with")
+
+        if _with is None:
+            with_sql = ""
+        elif not _with:
+            with_sql = " WITHOUT"
+        else:
+            with_sql = " WITH"
+
+        unique_sql = " UNIQUE KEYS" if expression.args.get("unique") else ""
+
+        return f"JSON{this}{with_sql}{unique_sql}"
diff --git a/sqlglot/parser.py b/sqlglot/parser.py
index 7644313b..0896677b 100644
--- a/sqlglot/parser.py
+++ b/sqlglot/parser.py
@@ -1252,6 +1252,8 @@ class Parser(metaclass=_Parser):
 
     COPY_INTO_VARLEN_OPTIONS = {"FILE_FORMAT", "COPY_OPTIONS", "FORMAT_OPTIONS", "CREDENTIAL"}
 
+    IS_JSON_PREDICATE_KIND = {"VALUE", "SCALAR", "ARRAY", "OBJECT"}
+
     STRICT_CAST = True
 
     PREFIXED_PIVOT_COLUMNS = False
@@ -4288,10 +4290,26 @@ class Parser(metaclass=_Parser):
             klass = exp.NullSafeEQ if negate else exp.NullSafeNEQ
             return self.expression(klass, this=this, expression=self._parse_bitwise())
 
-        expression = self._parse_null() or self._parse_boolean()
-        if not expression:
-            self._retreat(index)
-            return None
+        if self._match(TokenType.JSON):
+            kind = self._match_texts(self.IS_JSON_PREDICATE_KIND) and self._prev.text.upper()
+
+            if self._match_text_seq("WITH"):
+                _with = True
+            elif self._match_text_seq("WITHOUT"):
+                _with = False
+            else:
+                _with = None
+
+            unique = self._match(TokenType.UNIQUE)
+            self._match_text_seq("KEYS")
+            expression: t.Optional[exp.Expression] = self.expression(
+                exp.JSON, **{"this": kind, "with": _with, "unique": unique}
+            )
+        else:
+            expression = self._parse_null() or self._parse_boolean()
+            if not expression:
+                self._retreat(index)
+                return None
 
         this = self.expression(exp.Is, this=this, expression=expression)
         return self.expression(exp.Not, this=this) if negate else this


Test patch:
diff --git a/tests/dialects/test_postgres.py b/tests/dialects/test_postgres.py
index c681bcff..c628db46 100644
--- a/tests/dialects/test_postgres.py
+++ b/tests/dialects/test_postgres.py
@@ -788,6 +788,13 @@ class TestPostgres(Validator):
             },
         )
 
+        self.validate_identity(
+            'SELECT js, js IS JSON AS "json?", js IS JSON VALUE AS "scalar?", js IS JSON SCALAR AS "scalar?", js IS JSON OBJECT AS "object?", js IS JSON ARRAY AS "array?" FROM t'
+        )
+        self.validate_identity(
+            'SELECT js, js IS JSON ARRAY WITH UNIQUE KEYS AS "array w. UK?", js IS JSON ARRAY WITHOUT UNIQUE KEYS AS "array w/o UK?", js IS JSON ARRAY UNIQUE KEYS AS "array w UK 2?" FROM t'
+        )
+
     def test_ddl(self):
         # Checks that user-defined types are parsed into DataType instead of Identifier
         self.parse_one("CREATE TABLE t (a udt)").this.expressions[0].args["kind"].assert_is(