Repository: tobymao/sqlglot

Issue:
Oracle CONVERT function can not be parsed
**Context**
I can't parse Oracle SQL request when using a CONVERT function. I am using sqlglot _26.17.1_

**Code snippet**
```
sql_request = "SELECT CONVERT('hello', 'US7ASCII') FROM DUAL"
parsed = parse_one(sql_request, read="oracle")
```
I got the following error: 

```
ParseError                                Traceback (most recent call last)
Cell In[9], line 1
----> 1 parsed = parse_one(sql_request, read="oracle")

File /usr/local/lib/python3.11/site-packages/sqlglot/__init__.py:139, in parse_one(sql, read, dialect, into, **opts)
    137     result = dialect.parse_into(into, sql, **opts)
    138 else:
--> 139     result = dialect.parse(sql, **opts)
    141 for expression in result:
    142     if not expression:

File /usr/local/lib/python3.11/site-packages/sqlglot/dialects/dialect.py:920, in Dialect.parse(self, sql, **opts)
    919 def parse(self, sql: str, **opts) -> t.List[t.Optional[exp.Expression]]:
--> 920     return self.parser(**opts).parse(self.tokenize(sql), sql)

File /usr/local/lib/python3.11/site-packages/sqlglot/parser.py:1416, in Parser.parse(self, raw_tokens, sql)
   1402 def parse(
   1403     self, raw_tokens: t.List[Token], sql: t.Optional[str] = None
   1404 ) -> t.List[t.Optional[exp.Expression]]:
   1405     """
   1406     Parses a list of tokens and returns a list of syntax trees, one tree
   1407     per parsed SQL statement.
   (...)
   1414         The list of the produced syntax trees.
   1415     """
-> 1416     return self._parse(
   1417         parse_method=self.__class__._parse_statement, raw_tokens=raw_tokens, sql=sql
   1418     )

File /usr/local/lib/python3.11/site-packages/sqlglot/parser.py:1485, in Parser._parse(self, parse_method, raw_tokens, sql)
   1482 self._tokens = tokens
   1483 self._advance()
-> 1485 expressions.append(parse_method(self))
   1487 if self._index < len(self._tokens):
   1488     self.raise_error("Invalid expression / Unexpected token")

File /usr/local/lib/python3.11/site-packages/sqlglot/parser.py:1723, in Parser._parse_statement(self)
   1720     return self._parse_command()
   1722 expression = self._parse_expression()
-> 1723 expression = self._parse_set_operations(expression) if expression else self._parse_select()
   1724 return self._parse_query_modifiers(expression)

File /usr/local/lib/python3.11/site-packages/sqlglot/parser.py:2996, in Parser._parse_select(self, nested, table, parse_subquery_alias, parse_set_operation)
   2993     operation_modifiers.append(exp.var(self._prev.text.upper()))
   2995 limit = self._parse_limit(top=True)
-> 2996 projections = self._parse_projections()
   2998 this = self.expression(
   2999     exp.Select,
   3000     kind=kind,
   (...)
   3005     operation_modifiers=operation_modifiers or None,
   3006 )
   3007 this.comments = comments

File /usr/local/lib/python3.11/site-packages/sqlglot/parser.py:2936, in Parser._parse_projections(self)
   2935 def _parse_projections(self) -> t.List[exp.Expression]:
-> 2936     return self._parse_expressions()

File /usr/local/lib/python3.11/site-packages/sqlglot/parser.py:6632, in Parser._parse_expressions(self)
   6631 def _parse_expressions(self) -> t.List[exp.Expression]:
-> 6632     return self._parse_csv(self._parse_expression)

File /usr/local/lib/python3.11/site-packages/sqlglot/parser.py:6586, in Parser._parse_csv(self, parse_method, sep)
   6583 def _parse_csv(
   6584     self, parse_method: t.Callable, sep: TokenType = TokenType.COMMA
   6585 ) -> t.List[exp.Expression]:
-> 6586     parse_result = parse_method()
   6587     items = [parse_result] if parse_result is not None else []
   6589     while self._match(sep):

File /usr/local/lib/python3.11/site-packages/sqlglot/parser.py:4397, in Parser._parse_expression(self)
   4396 def _parse_expression(self) -> t.Optional[exp.Expression]:
-> 4397     return self._parse_alias(self._parse_assignment())

File /usr/local/lib/python3.11/site-packages/sqlglot/parser.py:4400, in Parser._parse_assignment(self)
   4399 def _parse_assignment(self) -> t.Optional[exp.Expression]:
-> 4400     this = self._parse_disjunction()
   4401     if not this and self._next and self._next.token_type in self.ASSIGNMENT:
   4402         # This allows us to parse <non-identifier token> := <expr>
   4403         this = exp.column(
   4404             t.cast(str, self._advance_any(ignore_reserved=True) and self._prev.text)
   4405         )

File /usr/local/lib/python3.11/site-packages/sqlglot/parser.py:4421, in Parser._parse_disjunction(self)
   4420 def _parse_disjunction(self) -> t.Optional[exp.Expression]:
-> 4421     return self._parse_tokens(self._parse_conjunction, self.DISJUNCTION)

File /usr/local/lib/python3.11/site-packages/sqlglot/parser.py:6600, in Parser._parse_tokens(self, parse_method, expressions)
   6597 def _parse_tokens(
   6598     self, parse_method: t.Callable, expressions: t.Dict
   6599 ) -> t.Optional[exp.Expression]:
-> 6600     this = parse_method()
   6602     while self._match_set(expressions):
   6603         this = self.expression(
   6604             expressions[self._prev.token_type],
   6605             this=this,
   6606             comments=self._prev_comments,
   6607             expression=parse_method(),
   6608         )

File /usr/local/lib/python3.11/site-packages/sqlglot/parser.py:4424, in Parser._parse_conjunction(self)
   4423 def _parse_conjunction(self) -> t.Optional[exp.Expression]:
-> 4424     return self._parse_tokens(self._parse_equality, self.CONJUNCTION)

File /usr/local/lib/python3.11/site-packages/sqlglot/parser.py:6600, in Parser._parse_tokens(self, parse_method, expressions)
   6597 def _parse_tokens(
   6598     self, parse_method: t.Callable, expressions: t.Dict
   6599 ) -> t.Optional[exp.Expression]:
-> 6600     this = parse_method()
   6602     while self._match_set(expressions):
   6603         this = self.expression(
   6604             expressions[self._prev.token_type],
   6605             this=this,
   6606             comments=self._prev_comments,
   6607             expression=parse_method(),
   6608         )

File /usr/local/lib/python3.11/site-packages/sqlglot/parser.py:4427, in Parser._parse_equality(self)
   4426 def _parse_equality(self) -> t.Optional[exp.Expression]:
-> 4427     return self._parse_tokens(self._parse_comparison, self.EQUALITY)

File /usr/local/lib/python3.11/site-packages/sqlglot/parser.py:6600, in Parser._parse_tokens(self, parse_method, expressions)
   6597 def _parse_tokens(
   6598     self, parse_method: t.Callable, expressions: t.Dict
   6599 ) -> t.Optional[exp.Expression]:
-> 6600     this = parse_method()
   6602     while self._match_set(expressions):
   6603         this = self.expression(
   6604             expressions[self._prev.token_type],
   6605             this=this,
   6606             comments=self._prev_comments,
   6607             expression=parse_method(),
   6608         )

File /usr/local/lib/python3.11/site-packages/sqlglot/parser.py:4430, in Parser._parse_comparison(self)
   4429 def _parse_comparison(self) -> t.Optional[exp.Expression]:
-> 4430     return self._parse_tokens(self._parse_range, self.COMPARISON)

File /usr/local/lib/python3.11/site-packages/sqlglot/parser.py:6600, in Parser._parse_tokens(self, parse_method, expressions)
   6597 def _parse_tokens(
   6598     self, parse_method: t.Callable, expressions: t.Dict
   6599 ) -> t.Optional[exp.Expression]:
-> 6600     this = parse_method()
   6602     while self._match_set(expressions):
   6603         this = self.expression(
   6604             expressions[self._prev.token_type],
   6605             this=this,
   6606             comments=self._prev_comments,
   6607             expression=parse_method(),
   6608         )

File /usr/local/lib/python3.11/site-packages/sqlglot/parser.py:4433, in Parser._parse_range(self, this)
   4432 def _parse_range(self, this: t.Optional[exp.Expression] = None) -> t.Optional[exp.Expression]:
-> 4433     this = this or self._parse_bitwise()
   4434     negate = self._match(TokenType.NOT)
   4436     if self._match_set(self.RANGE_PARSERS):

File /usr/local/lib/python3.11/site-packages/sqlglot/parser.py:4589, in Parser._parse_bitwise(self)
   4588 def _parse_bitwise(self) -> t.Optional[exp.Expression]:
-> 4589     this = self._parse_term()
   4591     while True:
   4592         if self._match_set(self.BITWISE):

File /usr/local/lib/python3.11/site-packages/sqlglot/parser.py:4623, in Parser._parse_term(self)
   4622 def _parse_term(self) -> t.Optional[exp.Expression]:
-> 4623     this = self._parse_factor()
   4625     while self._match_set(self.TERM):
   4626         klass = self.TERM[self._prev.token_type]

File /usr/local/lib/python3.11/site-packages/sqlglot/parser.py:4646, in Parser._parse_factor(self)
   4644 def _parse_factor(self) -> t.Optional[exp.Expression]:
   4645     parse_method = self._parse_exponent if self.EXPONENT else self._parse_unary
-> 4646     this = parse_method()
   4648     while self._match_set(self.FACTOR):
   4649         klass = self.FACTOR[self._prev.token_type]

File /usr/local/lib/python3.11/site-packages/sqlglot/parser.py:4671, in Parser._parse_unary(self)
   4669 if self._match_set(self.UNARY_PARSERS):
   4670     return self.UNARY_PARSERS[self._prev.token_type](self)
-> 4671 return self._parse_at_time_zone(self._parse_type())

File /usr/local/lib/python3.11/site-packages/sqlglot/parser.py:4723, in Parser._parse_type(self, parse_interval, fallback_to_identifier)
   4720 if fallback_to_identifier:
   4721     return self._parse_id_var()
-> 4723 this = self._parse_column()
   4724 return this and self._parse_column_ops(this)

File /usr/local/lib/python3.11/site-packages/sqlglot/parser.py:5006, in Parser._parse_column(self)
   5005 def _parse_column(self) -> t.Optional[exp.Expression]:
-> 5006     this = self._parse_column_reference()
   5007     column = self._parse_column_ops(this) if this else self._parse_bracket(this)
   5009     if self.dialect.SUPPORTS_COLUMN_JOIN_MARKS and column:

File /usr/local/lib/python3.11/site-packages/sqlglot/parser.py:5015, in Parser._parse_column_reference(self)
   5014 def _parse_column_reference(self) -> t.Optional[exp.Expression]:
-> 5015     this = self._parse_field()
   5016     if (
   5017         not this
   5018         and self._match(TokenType.VALUES, advance=False)
   5019         and self.VALUES_FOLLOWED_BY_PAREN
   5020         and (not self._next or self._next.token_type != TokenType.L_PAREN)
   5021     ):
   5022         this = self._parse_id_var()

File /usr/local/lib/python3.11/site-packages/sqlglot/parser.py:5203, in Parser._parse_field(self, any_token, tokens, anonymous_func)
   5198     field = (
   5199         self._parse_function(anonymous=anonymous_func, any_token=any_token)
   5200         or self._parse_primary()
   5201     )
   5202 else:
-> 5203     field = self._parse_primary() or self._parse_function(
   5204         anonymous=anonymous_func, any_token=any_token
   5205     )
   5206 return field or self._parse_id_var(any_token=any_token, tokens=tokens)

File /usr/local/lib/python3.11/site-packages/sqlglot/parser.py:5226, in Parser._parse_function(self, functions, anonymous, optional_parens, any_token)
   5223     self._advance(2)
   5224     fn_syntax = True
-> 5226 func = self._parse_function_call(
   5227     functions=functions,
   5228     anonymous=anonymous,
   5229     optional_parens=optional_parens,
   5230     any_token=any_token,
   5231 )
   5233 if fn_syntax:
   5234     self._match(TokenType.R_BRACE)

File /usr/local/lib/python3.11/site-packages/sqlglot/parser.py:5275, in Parser._parse_function_call(self, functions, anonymous, optional_parens, any_token)
   5273 parser = self.FUNCTION_PARSERS.get(upper)
   5274 if parser and not anonymous:
-> 5275     this = parser(self)
   5276 else:
   5277     subquery_predicate = self.SUBQUERY_PREDICATES.get(token_type)

File /usr/local/lib/python3.11/site-packages/sqlglot/parser.py:1112, in Parser.<lambda>(self)
   1106 FUNCTIONS_WITH_ALIASED_ARGS = {"STRUCT"}
   1108 KEY_VALUE_DEFINITIONS = (exp.Alias, exp.EQ, exp.PropertyEQ, exp.Slice)
   1110 FUNCTION_PARSERS = {
   1111     "CAST": lambda self: self._parse_cast(self.STRICT_CAST),
-> 1112     "CONVERT": lambda self: self._parse_convert(self.STRICT_CAST),
   1113     "DECODE": lambda self: self._parse_decode(),
   1114     "EXTRACT": lambda self: self._parse_extract(),
   1115     "GAP_FILL": lambda self: self._parse_gap_fill(),
   1116     "JSON_OBJECT": lambda self: self._parse_json_object(),
   1117     "JSON_OBJECTAGG": lambda self: self._parse_json_object(agg=True),
   1118     "JSON_TABLE": lambda self: self._parse_json_table(),
   1119     "MATCH": lambda self: self._parse_match_against(),
   1120     "NORMALIZE": lambda self: self._parse_normalize(),
   1121     "OPENJSON": lambda self: self._parse_open_json(),
   1122     "OVERLAY": lambda self: self._parse_overlay(),
   1123     "POSITION": lambda self: self._parse_position(),
   1124     "PREDICT": lambda self: self._parse_predict(),
   1125     "SAFE_CAST": lambda self: self._parse_cast(False, safe=True),
   1126     "STRING_AGG": lambda self: self._parse_string_agg(),
   1127     "SUBSTRING": lambda self: self._parse_substring(),
   1128     "TRIM": lambda self: self._parse_trim(),
   1129     "TRY_CAST": lambda self: self._parse_cast(False, safe=True),
   1130     "TRY_CONVERT": lambda self: self._parse_convert(False, safe=True),
   1131 }
   1133 QUERY_MODIFIER_PARSERS = {
   1134     TokenType.MATCH_RECOGNIZE: lambda self: ("match", self._parse_match_recognize()),
   1135     TokenType.PREWHERE: lambda self: ("prewhere", self._parse_prewhere()),
   (...)
   1159     TokenType.START_WITH: lambda self: ("connect", self._parse_connect()),
   1160 }
   1162 SET_PARSERS = {
   1163     "GLOBAL": lambda self: self._parse_set_item_assignment("GLOBAL"),
   1164     "LOCAL": lambda self: self._parse_set_item_assignment("LOCAL"),
   1165     "SESSION": lambda self: self._parse_set_item_assignment("SESSION"),
   1166     "TRANSACTION": lambda self: self._parse_set_transaction(),
   1167 }

File /usr/local/lib/python3.11/site-packages/sqlglot/parser.py:5992, in Parser._parse_convert(self, strict, safe)
   5989 else:
   5990     to = None
-> 5992 return self.expression(exp.Cast if strict else exp.TryCast, this=this, to=to, safe=safe)

File /usr/local/lib/python3.11/site-packages/sqlglot/parser.py:1549, in Parser.expression(self, exp_class, comments, **kwargs)
   1547 instance = exp_class(**kwargs)
   1548 instance.add_comments(comments) if comments else self._add_comments(instance)
-> 1549 return self.validate_expression(instance)

File /usr/local/lib/python3.11/site-packages/sqlglot/parser.py:1569, in Parser.validate_expression(self, expression, args)
   1567 if self.error_level != ErrorLevel.IGNORE:
   1568     for error_message in expression.error_messages(args):
-> 1569         self.raise_error(error_message)
   1571 return expression

File /usr/local/lib/python3.11/site-packages/sqlglot/parser.py:1529, in Parser.raise_error(self, message, token)
   1517 error = ParseError.new(
   1518     f"{message}. Line {token.line}, Col: {token.col}.\n"
   1519     f"  {start_context}\033[4m{highlight}\033[0m{end_context}",
   (...)
   1525     end_context=end_context,
   1526 )
   1528 if self.error_level == ErrorLevel.IMMEDIATE:
-> 1529     raise error
   1531 self.errors.append(error)

ParseError: Required keyword: 'to' missing for <class 'sqlglot.expressions.Cast'>. Line 1, Col: 34.
  select CONVERT('hello', 'US7ASCII') from dual

```
**Official Documentation**
Oracle documentation -> https://docs.oracle.com/en/database/oracle/oracle-database/19/sqlrf/CONVERT.html

Discussion:
@tchamwam: Thanks for your quick fix ! 

Solution patch:
diff --git a/sqlglot/dialects/oracle.py b/sqlglot/dialects/oracle.py
index 766977f2e9..e183db727d 100644
--- a/sqlglot/dialects/oracle.py
+++ b/sqlglot/dialects/oracle.py
@@ -110,6 +110,7 @@ class Parser(parser.Parser):
 
         FUNCTIONS = {
             **parser.Parser.FUNCTIONS,
+            "CONVERT": exp.ConvertToCharset.from_arg_list,
             "NVL": lambda args: build_coalesce(args, is_nvl=True),
             "SQUARE": lambda args: exp.Pow(this=seq_get(args, 0), expression=exp.Literal.number(2)),
             "TO_CHAR": build_timetostr_or_tochar,
@@ -141,6 +142,7 @@ class Parser(parser.Parser):
             ),
             "JSON_EXISTS": lambda self: self._parse_json_exists(),
         }
+        FUNCTION_PARSERS.pop("CONVERT")
 
         PROPERTY_PARSERS = {
             **parser.Parser.PROPERTY_PARSERS,
diff --git a/sqlglot/expressions.py b/sqlglot/expressions.py
index 5ade6534f6..ddf009535d 100644
--- a/sqlglot/expressions.py
+++ b/sqlglot/expressions.py
@@ -5495,6 +5495,11 @@ class Convert(Func):
     arg_types = {"this": True, "expression": True, "style": False}
 
 
+# https://docs.oracle.com/en/database/oracle/oracle-database/19/sqlrf/CONVERT.html
+class ConvertToCharset(Func):
+    arg_types = {"this": True, "dest": True, "source": False}
+
+
 class ConvertTimezone(Func):
     arg_types = {"source_tz": False, "target_tz": True, "timestamp": True}
 
diff --git a/sqlglot/generator.py b/sqlglot/generator.py
index 38f58235ba..7649b0f114 100644
--- a/sqlglot/generator.py
+++ b/sqlglot/generator.py
@@ -131,6 +131,9 @@ class Generator(metaclass=_Generator):
         exp.CollateColumnConstraint: lambda self, e: f"COLLATE {self.sql(e, 'this')}",
         exp.CommentColumnConstraint: lambda self, e: f"COMMENT {self.sql(e, 'this')}",
         exp.ConnectByRoot: lambda self, e: f"CONNECT_BY_ROOT {self.sql(e, 'this')}",
+        exp.ConvertToCharset: lambda self, e: self.func(
+            "CONVERT", e.this, e.args["dest"], e.args.get("source")
+        ),
         exp.CopyGrantsProperty: lambda *_: "COPY GRANTS",
         exp.CredentialsProperty: lambda self,
         e: f"CREDENTIALS=({self.expressions(e, 'expressions', sep=' ')})",


Test patch:
diff --git a/tests/dialects/test_oracle.py b/tests/dialects/test_oracle.py
index 945bf8a5fd..2671d719a5 100644
--- a/tests/dialects/test_oracle.py
+++ b/tests/dialects/test_oracle.py
@@ -523,6 +523,8 @@ def test_json_table(self):
 )) src""",
             pretty=True,
         )
+        self.validate_identity("CONVERT('foo', 'dst')")
+        self.validate_identity("CONVERT('foo', 'dst', 'src')")
 
     def test_connect_by(self):
         start = "START WITH last_name = 'King'"