Skip to content

Commit 8f5efc7

Browse files
mkmoisenmmoisen
andauthored
Feat(oracle): parse hints (#4249)
* feat(oracle)!: support oracle hints * feat(oracle): support oracle hints * feat(oracle)!: support oracle hints --------- Co-authored-by: mmoisen <[email protected]>
1 parent 5655cfb commit 8f5efc7

File tree

2 files changed

+124
-0
lines changed

2 files changed

+124
-0
lines changed

sqlglot/dialects/oracle.py

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
from sqlglot.helper import seq_get
1616
from sqlglot.parser import OPTIONS_TYPE, build_coalesce
1717
from sqlglot.tokens import TokenType
18+
from sqlglot.errors import ParseError
1819

1920
if t.TYPE_CHECKING:
2021
from sqlglot._typing import E
@@ -205,6 +206,57 @@ def _parse_json_array(self, expr_type: t.Type[E], **kwargs) -> E:
205206
)
206207

207208
def _parse_hint(self) -> t.Optional[exp.Hint]:
209+
start_index = self._index
210+
should_fallback_to_string = False
211+
212+
if not self._match(TokenType.HINT):
213+
return None
214+
215+
hints = []
216+
217+
try:
218+
for hint in iter(
219+
lambda: self._parse_csv(
220+
lambda: self._parse_hint_function_call() or self._parse_var(upper=True),
221+
),
222+
[],
223+
):
224+
hints.extend(hint)
225+
except ParseError:
226+
should_fallback_to_string = True
227+
228+
if not self._match_pair(TokenType.STAR, TokenType.SLASH):
229+
should_fallback_to_string = True
230+
231+
if should_fallback_to_string:
232+
self._retreat(start_index)
233+
return self._parse_hint_fallback_to_string()
234+
235+
return self.expression(exp.Hint, expressions=hints)
236+
237+
def _parse_hint_function_call(self) -> t.Optional[exp.Expression]:
238+
if not self._curr or not self._next or self._next.token_type != TokenType.L_PAREN:
239+
return None
240+
241+
this = self._curr.text
242+
243+
self._advance(2)
244+
args = self._parse_hint_args()
245+
this = self.expression(exp.Anonymous, this=this, expressions=args)
246+
self._match_r_paren(this)
247+
return this
248+
249+
def _parse_hint_args(self):
250+
args = []
251+
result = self._parse_var()
252+
253+
while result:
254+
args.append(result)
255+
result = self._parse_var()
256+
257+
return args
258+
259+
def _parse_hint_fallback_to_string(self) -> t.Optional[exp.Hint]:
208260
if self._match(TokenType.HINT):
209261
start = self._curr
210262
while self._curr and not self._match_pair(TokenType.STAR, TokenType.SLASH):
@@ -271,6 +323,7 @@ class Generator(generator.Generator):
271323
LAST_DAY_SUPPORTS_DATE_PART = False
272324
SUPPORTS_SELECT_INTO = True
273325
TZ_TO_WITH_TIME_ZONE = True
326+
QUERY_HINT_SEP = " "
274327

275328
TYPE_MAPPING = {
276329
**generator.Generator.TYPE_MAPPING,
@@ -370,3 +423,23 @@ def into_sql(self, expression: exp.Into) -> str:
370423
return f"{self.seg(into)} {self.sql(expression, 'this')}"
371424

372425
return f"{self.seg(into)} {self.expressions(expression)}"
426+
427+
def hint_sql(self, expression: exp.Hint) -> str:
428+
expressions = []
429+
430+
for expression in expression.expressions:
431+
if isinstance(expression, exp.Anonymous):
432+
formatted_args = self._format_hint_function_args(*expression.expressions)
433+
expressions.append(f"{self.sql(expression, 'this')}({formatted_args})")
434+
else:
435+
expressions.append(self.sql(expression))
436+
437+
return f" /*+ {self.expressions(sqls=expressions, sep=self.QUERY_HINT_SEP).strip()} */"
438+
439+
def _format_hint_function_args(self, *args: t.Optional[str | exp.Expression]) -> str:
440+
arg_sqls = tuple(self.sql(arg) for arg in args)
441+
if self.pretty and self.too_wide(arg_sqls):
442+
return self.indent(
443+
"\n" + "\n".join(arg_sqls) + "\n", skip_first=True, skip_last=True
444+
)
445+
return " ".join(arg_sqls)

tests/dialects/test_oracle.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -329,6 +329,57 @@ def test_hints(self):
329329
)
330330
self.validate_identity("INSERT /*+ APPEND */ INTO IAP_TBL (id, col1) VALUES (2, 'test2')")
331331
self.validate_identity("INSERT /*+ APPEND_VALUES */ INTO dest_table VALUES (i, 'Value')")
332+
self.validate_identity(
333+
"SELECT /*+ LEADING(departments employees) USE_NL(employees) */ * FROM employees JOIN departments ON employees.department_id = departments.department_id",
334+
"""SELECT /*+ LEADING(departments employees)
335+
USE_NL(employees) */
336+
*
337+
FROM employees
338+
JOIN departments
339+
ON employees.department_id = departments.department_id""",
340+
pretty=True,
341+
)
342+
self.validate_identity(
343+
"SELECT /*+ USE_NL(bbbbbbbbbbbbbbbbbbbbbbbb) LEADING(aaaaaaaaaaaaaaaaaaaaaaaa bbbbbbbbbbbbbbbbbbbbbbbb cccccccccccccccccccccccc dddddddddddddddddddddddd) INDEX(cccccccccccccccccccccccc) */ * FROM aaaaaaaaaaaaaaaaaaaaaaaa JOIN bbbbbbbbbbbbbbbbbbbbbbbb ON aaaaaaaaaaaaaaaaaaaaaaaa.id = bbbbbbbbbbbbbbbbbbbbbbbb.a_id JOIN cccccccccccccccccccccccc ON bbbbbbbbbbbbbbbbbbbbbbbb.id = cccccccccccccccccccccccc.b_id JOIN dddddddddddddddddddddddd ON cccccccccccccccccccccccc.id = dddddddddddddddddddddddd.c_id",
344+
)
345+
self.validate_identity(
346+
"SELECT /*+ USE_NL(bbbbbbbbbbbbbbbbbbbbbbbb) LEADING(aaaaaaaaaaaaaaaaaaaaaaaa bbbbbbbbbbbbbbbbbbbbbbbb cccccccccccccccccccccccc dddddddddddddddddddddddd) INDEX(cccccccccccccccccccccccc) */ * FROM aaaaaaaaaaaaaaaaaaaaaaaa JOIN bbbbbbbbbbbbbbbbbbbbbbbb ON aaaaaaaaaaaaaaaaaaaaaaaa.id = bbbbbbbbbbbbbbbbbbbbbbbb.a_id JOIN cccccccccccccccccccccccc ON bbbbbbbbbbbbbbbbbbbbbbbb.id = cccccccccccccccccccccccc.b_id JOIN dddddddddddddddddddddddd ON cccccccccccccccccccccccc.id = dddddddddddddddddddddddd.c_id",
347+
"""SELECT /*+ USE_NL(bbbbbbbbbbbbbbbbbbbbbbbb)
348+
LEADING(
349+
aaaaaaaaaaaaaaaaaaaaaaaa
350+
bbbbbbbbbbbbbbbbbbbbbbbb
351+
cccccccccccccccccccccccc
352+
dddddddddddddddddddddddd
353+
)
354+
INDEX(cccccccccccccccccccccccc) */
355+
*
356+
FROM aaaaaaaaaaaaaaaaaaaaaaaa
357+
JOIN bbbbbbbbbbbbbbbbbbbbbbbb
358+
ON aaaaaaaaaaaaaaaaaaaaaaaa.id = bbbbbbbbbbbbbbbbbbbbbbbb.a_id
359+
JOIN cccccccccccccccccccccccc
360+
ON bbbbbbbbbbbbbbbbbbbbbbbb.id = cccccccccccccccccccccccc.b_id
361+
JOIN dddddddddddddddddddddddd
362+
ON cccccccccccccccccccccccc.id = dddddddddddddddddddddddd.c_id""",
363+
pretty=True,
364+
)
365+
# Test that parsing error with keywords like select where etc falls back
366+
self.validate_identity(
367+
"SELECT /*+ LEADING(departments employees) USE_NL(employees) select where group by is order by */ * FROM employees JOIN departments ON employees.department_id = departments.department_id",
368+
"""SELECT /*+ LEADING(departments employees) USE_NL(employees) select where group by is order by */
369+
*
370+
FROM employees
371+
JOIN departments
372+
ON employees.department_id = departments.department_id""",
373+
pretty=True,
374+
)
375+
# Test that parsing error with , inside hint function falls back
376+
self.validate_identity(
377+
"SELECT /*+ LEADING(departments, employees) */ * FROM employees JOIN departments ON employees.department_id = departments.department_id"
378+
)
379+
# Test that parsing error with keyword inside hint function falls back
380+
self.validate_identity(
381+
"SELECT /*+ LEADING(departments select) */ * FROM employees JOIN departments ON employees.department_id = departments.department_id"
382+
)
332383

333384
def test_xml_table(self):
334385
self.validate_identity("XMLTABLE('x')")

0 commit comments

Comments
 (0)