Skip to content

Commit c905ee0

Browse files
authored
Support CONVERT expressions (#1048)
1 parent c0c2d58 commit c905ee0

File tree

7 files changed

+124
-0
lines changed

7 files changed

+124
-0
lines changed

src/ast/mod.rs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -473,6 +473,17 @@ pub enum Expr {
473473
},
474474
/// Unary operation e.g. `NOT foo`
475475
UnaryOp { op: UnaryOperator, expr: Box<Expr> },
476+
/// CONVERT a value to a different data type or character encoding `CONVERT(foo USING utf8mb4)`
477+
Convert {
478+
/// The expression to convert
479+
expr: Box<Expr>,
480+
/// The target data type
481+
data_type: Option<DataType>,
482+
/// The target character encoding
483+
charset: Option<ObjectName>,
484+
/// whether the target comes before the expr (MSSQL syntax)
485+
target_before_value: bool,
486+
},
476487
/// CAST an expression to a different data type e.g. `CAST(foo AS VARCHAR(123))`
477488
Cast {
478489
expr: Box<Expr>,
@@ -844,6 +855,28 @@ impl fmt::Display for Expr {
844855
write!(f, "{op}{expr}")
845856
}
846857
}
858+
Expr::Convert {
859+
expr,
860+
target_before_value,
861+
data_type,
862+
charset,
863+
} => {
864+
write!(f, "CONVERT(")?;
865+
if let Some(data_type) = data_type {
866+
if let Some(charset) = charset {
867+
write!(f, "{expr}, {data_type} CHARACTER SET {charset}")
868+
} else if *target_before_value {
869+
write!(f, "{data_type}, {expr}")
870+
} else {
871+
write!(f, "{expr}, {data_type}")
872+
}
873+
} else if let Some(charset) = charset {
874+
write!(f, "{expr} USING {charset}")
875+
} else {
876+
write!(f, "{expr}") // This should never happen
877+
}?;
878+
write!(f, ")")
879+
}
847880
Expr::Cast {
848881
expr,
849882
data_type,

src/dialect/mod.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,11 @@ pub trait Dialect: Debug + Any {
128128
fn supports_in_empty_list(&self) -> bool {
129129
false
130130
}
131+
/// Returns true if the dialect has a CONVERT function which accepts a type first
132+
/// and an expression second, e.g. `CONVERT(varchar, 1)`
133+
fn convert_type_before_value(&self) -> bool {
134+
false
135+
}
131136
/// Dialect-specific prefix parser override
132137
fn parse_prefix(&self, _parser: &mut Parser) -> Option<Result<Expr, ParserError>> {
133138
// return None to fall back to the default behavior

src/dialect/mssql.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,12 @@ impl Dialect for MsSqlDialect {
3535
|| ch == '_'
3636
}
3737

38+
/// SQL Server has `CONVERT(type, value)` instead of `CONVERT(value, type)`
39+
/// <https://learn.microsoft.com/en-us/sql/t-sql/functions/cast-and-convert-transact-sql?view=sql-server-ver16>
40+
fn convert_type_before_value(&self) -> bool {
41+
true
42+
}
43+
3844
fn supports_substring_from_for_expr(&self) -> bool {
3945
false
4046
}

src/dialect/redshift.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,4 +53,10 @@ impl Dialect for RedshiftSqlDialect {
5353
// Extends Postgres dialect with sharp
5454
PostgreSqlDialect {}.is_identifier_part(ch) || ch == '#'
5555
}
56+
57+
/// redshift has `CONVERT(type, value)` instead of `CONVERT(value, type)`
58+
/// <https://docs.aws.amazon.com/redshift/latest/dg/r_CONVERT_function.html>
59+
fn convert_type_before_value(&self) -> bool {
60+
true
61+
}
5662
}

src/parser/mod.rs

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -821,6 +821,7 @@ impl<'a> Parser<'a> {
821821
self.parse_time_functions(ObjectName(vec![w.to_ident()]))
822822
}
823823
Keyword::CASE => self.parse_case_expr(),
824+
Keyword::CONVERT => self.parse_convert_expr(),
824825
Keyword::CAST => self.parse_cast_expr(),
825826
Keyword::TRY_CAST => self.parse_try_cast_expr(),
826827
Keyword::SAFE_CAST => self.parse_safe_cast_expr(),
@@ -1227,6 +1228,57 @@ impl<'a> Parser<'a> {
12271228
}
12281229
}
12291230

1231+
/// mssql-like convert function
1232+
fn parse_mssql_convert(&mut self) -> Result<Expr, ParserError> {
1233+
self.expect_token(&Token::LParen)?;
1234+
let data_type = self.parse_data_type()?;
1235+
self.expect_token(&Token::Comma)?;
1236+
let expr = self.parse_expr()?;
1237+
self.expect_token(&Token::RParen)?;
1238+
Ok(Expr::Convert {
1239+
expr: Box::new(expr),
1240+
data_type: Some(data_type),
1241+
charset: None,
1242+
target_before_value: true,
1243+
})
1244+
}
1245+
1246+
/// Parse a SQL CONVERT function:
1247+
/// - `CONVERT('héhé' USING utf8mb4)` (MySQL)
1248+
/// - `CONVERT('héhé', CHAR CHARACTER SET utf8mb4)` (MySQL)
1249+
/// - `CONVERT(DECIMAL(10, 5), 42)` (MSSQL) - the type comes first
1250+
pub fn parse_convert_expr(&mut self) -> Result<Expr, ParserError> {
1251+
if self.dialect.convert_type_before_value() {
1252+
return self.parse_mssql_convert();
1253+
}
1254+
self.expect_token(&Token::LParen)?;
1255+
let expr = self.parse_expr()?;
1256+
if self.parse_keyword(Keyword::USING) {
1257+
let charset = self.parse_object_name()?;
1258+
self.expect_token(&Token::RParen)?;
1259+
return Ok(Expr::Convert {
1260+
expr: Box::new(expr),
1261+
data_type: None,
1262+
charset: Some(charset),
1263+
target_before_value: false,
1264+
});
1265+
}
1266+
self.expect_token(&Token::Comma)?;
1267+
let data_type = self.parse_data_type()?;
1268+
let charset = if self.parse_keywords(&[Keyword::CHARACTER, Keyword::SET]) {
1269+
Some(self.parse_object_name()?)
1270+
} else {
1271+
None
1272+
};
1273+
self.expect_token(&Token::RParen)?;
1274+
Ok(Expr::Convert {
1275+
expr: Box::new(expr),
1276+
data_type: Some(data_type),
1277+
charset,
1278+
target_before_value: false,
1279+
})
1280+
}
1281+
12301282
/// Parse a SQL CAST function e.g. `CAST(expr AS FLOAT)`
12311283
pub fn parse_cast_expr(&mut self) -> Result<Expr, ParserError> {
12321284
self.expect_token(&Token::LParen)?;

tests/sqlparser_mssql.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -475,6 +475,13 @@ fn parse_cast_varchar_max() {
475475
ms_and_generic().verified_expr("CAST('foo' AS VARCHAR(MAX))");
476476
}
477477

478+
#[test]
479+
fn parse_convert() {
480+
ms().verified_expr("CONVERT(VARCHAR(MAX), 'foo')");
481+
ms().verified_expr("CONVERT(VARCHAR(10), 'foo')");
482+
ms().verified_expr("CONVERT(DECIMAL(10,5), 12.55)");
483+
}
484+
478485
#[test]
479486
fn parse_similar_to() {
480487
fn chk(negated: bool) {

tests/sqlparser_mysql.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1843,3 +1843,18 @@ fn parse_drop_temporary_table() {
18431843
_ => unreachable!(),
18441844
}
18451845
}
1846+
1847+
#[test]
1848+
fn parse_convert_using() {
1849+
// https://dev.mysql.com/doc/refman/8.0/en/cast-functions.html#function_convert
1850+
1851+
// CONVERT(expr USING transcoding_name)
1852+
mysql().verified_only_select("SELECT CONVERT('x' USING latin1)");
1853+
mysql().verified_only_select("SELECT CONVERT(my_column USING utf8mb4) FROM my_table");
1854+
1855+
// CONVERT(expr, type)
1856+
mysql().verified_only_select("SELECT CONVERT('abc', CHAR(60))");
1857+
mysql().verified_only_select("SELECT CONVERT(123.456, DECIMAL(5,2))");
1858+
// with a type + a charset
1859+
mysql().verified_only_select("SELECT CONVERT('test', CHAR CHARACTER SET utf8mb4)");
1860+
}

0 commit comments

Comments
 (0)