Repository: tobymao/sqlglot

Issue:
ParseError with Oracle SQL containing hint USE_NL with more than 2 tables (version 11.4.1)

`parse_one(sql='SELECT  /*+ USE_NL(A B C) */ A.COL_TEST FROM TABLE_A A, TABLE_B B, TABLE_C C', read='oracle')
`
is returning: 
Traceback (most recent call last):
  File "/Applications/PyCharm CE.app/Contents/plugins/python-ce/helpers/pydev/pydevconsole.py", line 364, in runcode
    coro = func()
           ^^^^^^
  File "<input>", line 1, in <module>
  File "/usr/local/lib/python3.11/site-packages/sqlglot/__init__.py", line 147, in parse_one
    result = dialect.parse(sql, **opts)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.11/site-packages/sqlglot/dialects/dialect.py", line 163, in parse
    return self.parser(**opts).parse(self.tokenizer.tokenize(sql), sql)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.11/site-packages/sqlglot/parser.py", line 767, in parse
    return self._parse(
           ^^^^^^^^^^^^
  File "/usr/local/lib/python3.11/site-packages/sqlglot/parser.py", line 830, in _parse
    expressions.append(parse_method(self))
                       ^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.11/site-packages/sqlglot/parser.py", line 993, in _parse_statement
    expression = self._parse_set_operations(expression) if expression else self._parse_select()
                                                                           ^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.11/site-packages/sqlglot/parser.py", line 1689, in _parse_select
    hint = self._parse_hint()
           ^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.11/site-packages/sqlglot/parser.py", line 1831, in _parse_hint
    hints = self._parse_csv(self._parse_function)
            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.11/site-packages/sqlglot/parser.py", line 3585, in _parse_csv
    parse_result = parse_method()
                   ^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.11/site-packages/sqlglot/parser.py", line 2842, in _parse_function
    self._match_r_paren(this)
  File "/usr/local/lib/python3.11/site-packages/sqlglot/parser.py", line 3935, in _match_r_paren
    self.raise_error("Expecting )")
  File "/usr/local/lib/python3.11/site-packages/sqlglot/parser.py", line 876, in raise_error
    raise error
sqlglot.errors.ParseError: Expecting ). Line 1, Col: 24.
  SELECT  /*+ USE_NL(A B C) */ A.COL_TEST FROM TABLE_A A, TABLE_B B, TABLE_C C

Or
`parse_one(sql='SELECT  /*+ USE_NL(A B) */ A.COL_TEST FROM TABLE_A A, TABLE_B B, TABLE_C C', read='oracle')`

works and returns:

(SELECT hint: 
  (HINT expressions: 
    (ANONYMOUS this: USE_NL, expressions: 
      (ALIAS this: 
        (COLUMN this: 
          (IDENTIFIER this: A, quoted: False)), alias: 
        (IDENTIFIER this: B, quoted: False)))), expressions: 
  (COLUMN this: 
    (IDENTIFIER this: COL_TEST, quoted: False), table: 
    (IDENTIFIER this: A, quoted: False)), from: 
  (FROM expressions: 
    (TABLE this: 
      (IDENTIFIER this: TABLE_A, quoted: False), alias: 
      (TABLEALIAS this: 
        (IDENTIFIER this: A, quoted: False))), 
    (TABLE this: 
      (IDENTIFIER this: TABLE_B, quoted: False), alias: 
      (TABLEALIAS this: 
        (IDENTIFIER this: B, quoted: False))), 
    (TABLE this: 
      (IDENTIFIER this: TABLE_C, quoted: False), alias: 
      (TABLEALIAS this: 
        (IDENTIFIER this: C, quoted: False)))))


Discussion:
Do all hint functions in Oracle SQL contain spaces as separators instead of commas? I'm not familiar with this syntax, can you provide some more information?
I'm a beginner with Oracle, but the documentation seems to show that all hint with multiple values are using space to separate values:
https://docs.oracle.com/cd/B12037_01/server.101/b10752/hintsref.htm#29911
```
SELECT /*+ NO_INDEX(employees emp_empid) */ employee_id 
  FROM employees 
  WHERE employee_id > 200; 
```
```
SELECT /*+ NO_INDEX_FFS(items item_order_ix) */ order_id
  FROM order_items items;
```
```
SELECT /*+ LEADING(e j) */ *
    FROM employees e, departments d, job_history j
    WHERE e.department_id = d.department_id
      AND e.hire_date = j.start_date;
```

And you can find more in the link. 
Also,
You can notice that:
`parse_one(sql='SELECT  /*+ USE_NL(A B) */ A.COL_TEST FROM TABLE_A A, TABLE_B B, TABLE_C C', read='oracle')`
is working even if A and B are separate by space and not comma in the USE_NL hint.
> I'm a beginner with Oracle, but the documentation seems to show that all hint with multiple values are using space to separate values: https://docs.oracle.com/cd/B12037_01/server.101/b10752/hintsref.htm#29911
> [ ... ]
> And you can find more in the link.

Thanks, I'll take a look.

Regarding the `USE_NL(A B)` example: even though it parses, the resulting AST is not correct because `A B` is parsed into an alias node, i.e. if you generate sql for it afterwards it will convert it to `A AS B` instead of `A B`.
Oh gosh. Should I write an issue on that too ? Or will it be fixed when we will be able to use multiple tables inside an hint? 
No need, it's generally the expected behavior. It'll probably be fixed too after I fix the hint parsing for Oracle.

Resolving patch:
diff --git a/sqlglot/dialects/oracle.py b/sqlglot/dialects/oracle.py
--- a/sqlglot/dialects/oracle.py
+++ b/sqlglot/dialects/oracle.py
@@ -89,6 +89,20 @@ def _parse_column(self) -> t.Optional[exp.Expression]:
                 column.set("join_mark", self._match(TokenType.JOIN_MARKER))
             return column
 
+        def _parse_hint(self) -> t.Optional[exp.Expression]:
+            if self._match(TokenType.HINT):
+                start = self._curr
+                while self._curr and not self._match_pair(TokenType.STAR, TokenType.SLASH):
+                    self._advance()
+
+                if not self._curr:
+                    self.raise_error("Expected */ after HINT")
+
+                end = self._tokens[self._index - 3]
+                return exp.Hint(expressions=[self._find_sql(start, end)])
+
+            return None
+
     class Generator(generator.Generator):
         LOCKING_READS_SUPPORTED = True
 
@@ -110,17 +124,18 @@ class Generator(generator.Generator):
         TRANSFORMS = {
             **generator.Generator.TRANSFORMS,  # type: ignore
             **transforms.UNALIAS_GROUP,  # type: ignore
+            exp.Hint: lambda self, e: f" /*+ {self.expressions(e).strip()} */",
             exp.ILike: no_ilike_sql,
             exp.Limit: _limit_sql,
-            exp.Trim: trim_sql,
             exp.Matches: rename_func("DECODE"),
             exp.StrToTime: lambda self, e: f"TO_TIMESTAMP({self.sql(e, 'this')}, {self.format_time(e)})",
             exp.Subquery: lambda self, e: self.subquery_sql(e, sep=" "),
+            exp.Substring: rename_func("SUBSTR"),
             exp.Table: lambda self, e: self.table_sql(e, sep=" "),
             exp.TimeToStr: lambda self, e: f"TO_CHAR({self.sql(e, 'this')}, {self.format_time(e)})",
-            exp.UnixToTime: lambda self, e: f"TO_DATE('1970-01-01','YYYY-MM-DD') + ({self.sql(e, 'this')} / 86400)",
-            exp.Substring: rename_func("SUBSTR"),
             exp.ToChar: lambda self, e: self.function_fallback_sql(e),
+            exp.Trim: trim_sql,
+            exp.UnixToTime: lambda self, e: f"TO_DATE('1970-01-01','YYYY-MM-DD') + ({self.sql(e, 'this')} / 86400)",
         }
 
         def query_modifiers(self, expression: exp.Expression, *sqls: str) -> str:


Test patch:
diff --git a/tests/dialects/test_oracle.py b/tests/dialects/test_oracle.py
--- a/tests/dialects/test_oracle.py
+++ b/tests/dialects/test_oracle.py
@@ -12,6 +12,24 @@ def test_join_marker(self):
         self.validate_identity("SELECT e1.x, e2.x FROM e e1, e e2 WHERE e1.y = e2.y (+)")
         self.validate_identity("SELECT e1.x, e2.x FROM e e1, e e2 WHERE e1.y (+) = e2.y (+)")
 
+    def test_hints(self):
+        self.validate_identity("SELECT /*+ USE_NL(A B) */ A.COL_TEST FROM TABLE_A A, TABLE_B B")
+        self.validate_identity(
+            "SELECT /*+ INDEX(v.j jhist_employee_ix (employee_id start_date)) */ * FROM v"
+        )
+        self.validate_identity(
+            "SELECT /*+ USE_NL(A B C) */ A.COL_TEST FROM TABLE_A A, TABLE_B B, TABLE_C C"
+        )
+        self.validate_identity(
+            "SELECT /*+ NO_INDEX(employees emp_empid) */ employee_id FROM employees WHERE employee_id > 200"
+        )
+        self.validate_identity(
+            "SELECT /*+ NO_INDEX_FFS(items item_order_ix) */ order_id FROM order_items items"
+        )
+        self.validate_identity(
+            "SELECT /*+ LEADING(e j) */ * FROM employees e, departments d, job_history j WHERE e.department_id = d.department_id AND e.hire_date = j.start_date"
+        )
+
     def test_xml_table(self):
         self.validate_identity("XMLTABLE('x')")
         self.validate_identity("XMLTABLE('x' RETURNING SEQUENCE BY REF)")