Repository: tobymao/sqlglot

Issue:
Parsing oracle queries having SQL/JSON functions like JSON_*
I have been trying to parse our Oracle queries using sqlglot. It's been very useful so far. 
I have observed that sqlglot parsing fails for few SQL/JSON functions like JSON_OBJECT, JSON_OBJECTAGG, JSON_ARRAY, JSON_TABLE, JSON_VALUE etc.

**Sample oracle query snippet - ** 
```
JSON_TABLE(res,'$.info[*]' COLUMNS (
tempid NUMBER PATH '$.tempid',
NESTED PATH '$.calid[*]' COLUMNS (
last_dt PATH '$.last_dt '))) src
```
For above query, sqlglot.parse_one method fails to parse. Similarly, it doesn't parse other JSON methods too.

**Official Documentation -**
https://docs.oracle.com/en/database/oracle/oracle-database/19/sqlrf/JSON_TABLE.html#GUID-09422D4A-936C-4D38-9991-C64101283D98

Request team to look into this. Thank you!

Comments:

Patch:
diff --git a/sqlglot/expressions.py b/sqlglot/expressions.py
index 5627af8d..314995ea 100644
--- a/sqlglot/expressions.py
+++ b/sqlglot/expressions.py
@@ -4633,14 +4633,18 @@ class JSONArrayAgg(Func):
 # https://docs.oracle.com/en/database/oracle/oracle-database/19/sqlrf/JSON_TABLE.html
 # Note: parsing of JSON column definitions is currently incomplete.
 class JSONColumnDef(Expression):
-    arg_types = {"this": True, "kind": False, "path": False}
+    arg_types = {"this": False, "kind": False, "path": False, "nested_schema": False}
+
+
+class JSONSchema(Expression):
+    arg_types = {"expressions": True}
 
 
 # # https://docs.oracle.com/en/database/oracle/oracle-database/19/sqlrf/JSON_TABLE.html
 class JSONTable(Func):
     arg_types = {
         "this": True,
-        "expressions": True,
+        "schema": True,
         "path": False,
         "error_handling": False,
         "empty_handling": False,
diff --git a/sqlglot/generator.py b/sqlglot/generator.py
index 7a2879ce..8257adc8 100644
--- a/sqlglot/generator.py
+++ b/sqlglot/generator.py
@@ -2179,13 +2179,21 @@ class Generator:
         )
 
     def jsoncolumndef_sql(self, expression: exp.JSONColumnDef) -> str:
+        path = self.sql(expression, "path")
+        path = f" PATH {path}" if path else ""
+        nested_schema = self.sql(expression, "nested_schema")
+
+        if nested_schema:
+            return f"NESTED{path} {nested_schema}"
+
         this = self.sql(expression, "this")
         kind = self.sql(expression, "kind")
         kind = f" {kind}" if kind else ""
-        path = self.sql(expression, "path")
-        path = f" PATH {path}" if path else ""
         return f"{this}{kind}{path}"
 
+    def jsonschema_sql(self, expression: exp.JSONSchema) -> str:
+        return self.func("COLUMNS", *expression.expressions)
+
     def jsontable_sql(self, expression: exp.JSONTable) -> str:
         this = self.sql(expression, "this")
         path = self.sql(expression, "path")
@@ -2194,9 +2202,9 @@ class Generator:
         error_handling = f" {error_handling}" if error_handling else ""
         empty_handling = expression.args.get("empty_handling")
         empty_handling = f" {empty_handling}" if empty_handling else ""
-        columns = f" COLUMNS ({self.expressions(expression, skip_first=True)})"
+        schema = self.sql(expression, "schema")
         return self.func(
-            "JSON_TABLE", this, suffix=f"{path}{error_handling}{empty_handling}{columns})"
+            "JSON_TABLE", this, suffix=f"{path}{error_handling}{empty_handling} {schema})"
         )
 
     def openjsoncolumndef_sql(self, expression: exp.OpenJSONColumnDef) -> str:
diff --git a/sqlglot/parser.py b/sqlglot/parser.py
index b0eb3ff0..cf9820b2 100644
--- a/sqlglot/parser.py
+++ b/sqlglot/parser.py
@@ -4354,22 +4354,43 @@ class Parser(metaclass=_Parser):
 
     # Note: this is currently incomplete; it only implements the "JSON_value_column" part
     def _parse_json_column_def(self) -> exp.JSONColumnDef:
-        this = self._parse_id_var()
-        kind = self._parse_types(allow_identifiers=False)
+        if not self._match_text_seq("NESTED"):
+            this = self._parse_id_var()
+            kind = self._parse_types(allow_identifiers=False)
+            nested = None
+        else:
+            this = None
+            kind = None
+            nested = True
+
         path = self._match_text_seq("PATH") and self._parse_string()
-        return self.expression(exp.JSONColumnDef, this=this, kind=kind, path=path)
+        nested_schema = nested and self._parse_json_schema()
+
+        return self.expression(
+            exp.JSONColumnDef,
+            this=this,
+            kind=kind,
+            path=path,
+            nested_schema=nested_schema,
+        )
+
+    def _parse_json_schema(self) -> exp.JSONSchema:
+        self._match_text_seq("COLUMNS")
+        return self.expression(
+            exp.JSONSchema,
+            expressions=self._parse_wrapped_csv(self._parse_json_column_def, optional=True),
+        )
 
     def _parse_json_table(self) -> exp.JSONTable:
         this = self._parse_format_json(self._parse_bitwise())
         path = self._match(TokenType.COMMA) and self._parse_string()
         error_handling = self._parse_on_handling("ERROR", "ERROR", "NULL")
         empty_handling = self._parse_on_handling("EMPTY", "ERROR", "NULL")
-        self._match_text_seq("COLUMNS")
-        expressions = self._parse_wrapped_csv(self._parse_json_column_def, optional=True)
+        schema = self._parse_json_schema()
 
         return exp.JSONTable(
             this=this,
-            expressions=expressions,
+            schema=schema,
             path=path,
             error_handling=error_handling,
             empty_handling=empty_handling,


Test patch:
diff --git a/tests/dialects/test_mysql.py b/tests/dialects/test_mysql.py
index df52bd46..b9d1d26a 100644
--- a/tests/dialects/test_mysql.py
+++ b/tests/dialects/test_mysql.py
@@ -66,7 +66,7 @@ class TestMySQL(Validator):
             "INSERT INTO x VALUES (1, 'a', 2.0) ON DUPLICATE KEY UPDATE x.id = 1"
         )
         self.validate_identity(
-            "CREATE OR REPLACE VIEW my_view AS SELECT column1 AS `boo`, column2 AS `foo` FROM my_table WHERE column3 = 'some_value' UNION SELECT q.* FROM fruits_table, JSON_TABLE(Fruits, '$[*]' COLUMNS (id VARCHAR(255) PATH '$.$id', value VARCHAR(255) PATH '$.value')) AS q",
+            "CREATE OR REPLACE VIEW my_view AS SELECT column1 AS `boo`, column2 AS `foo` FROM my_table WHERE column3 = 'some_value' UNION SELECT q.* FROM fruits_table, JSON_TABLE(Fruits, '$[*]' COLUMNS(id VARCHAR(255) PATH '$.$id', value VARCHAR(255) PATH '$.value')) AS q",
         )
 
         self.validate_all(
diff --git a/tests/dialects/test_oracle.py b/tests/dialects/test_oracle.py
index 5572ec10..d92eea5f 100644
--- a/tests/dialects/test_oracle.py
+++ b/tests/dialects/test_oracle.py
@@ -234,21 +234,30 @@ MATCH_RECOGNIZE (
 
     def test_json_table(self):
         self.validate_identity(
-            "SELECT * FROM JSON_TABLE(foo FORMAT JSON, 'bla' ERROR ON ERROR NULL ON EMPTY COLUMNS (foo PATH 'bar'))"
+            "SELECT * FROM JSON_TABLE(foo FORMAT JSON, 'bla' ERROR ON ERROR NULL ON EMPTY COLUMNS(foo PATH 'bar'))"
         )
         self.validate_identity(
             "SELECT * FROM JSON_TABLE(foo FORMAT JSON, 'bla' ERROR ON ERROR NULL ON EMPTY COLUMNS foo PATH 'bar')",
-            "SELECT * FROM JSON_TABLE(foo FORMAT JSON, 'bla' ERROR ON ERROR NULL ON EMPTY COLUMNS (foo PATH 'bar'))",
+            "SELECT * FROM JSON_TABLE(foo FORMAT JSON, 'bla' ERROR ON ERROR NULL ON EMPTY COLUMNS(foo PATH 'bar'))",
         )
         self.validate_identity(
             """SELECT
   CASE WHEN DBMS_LOB.GETLENGTH(info) < 32000 THEN DBMS_LOB.SUBSTR(info) END AS info_txt,
   info AS info_clob
 FROM schemaname.tablename ar
-INNER JOIN JSON_TABLE(:emps, '$[*]' COLUMNS (empno NUMBER PATH '$')) jt
+INNER JOIN JSON_TABLE(:emps, '$[*]' COLUMNS(empno NUMBER PATH '$')) jt
   ON ar.empno = jt.empno""",
             pretty=True,
         )
+        self.validate_identity(
+            """SELECT
+  *
+FROM JSON_TABLE(res, '$.info[*]' COLUMNS(
+  tempid NUMBER PATH '$.tempid',
+  NESTED PATH '$.calid[*]' COLUMNS(last_dt PATH '$.last_dt ')
+)) src""",
+            pretty=True,
+        )
 
     def test_connect_by(self):
         start = "START WITH last_name = 'King'"