Support CONVERT expressions (#1048)

This commit is contained in:
Ophir LOJKINE 2023-11-20 20:55:18 +01:00 committed by GitHub
parent c0c2d58910
commit c905ee0cb8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 124 additions and 0 deletions

View file

@ -473,6 +473,17 @@ pub enum Expr {
}, },
/// Unary operation e.g. `NOT foo` /// Unary operation e.g. `NOT foo`
UnaryOp { op: UnaryOperator, expr: Box<Expr> }, UnaryOp { op: UnaryOperator, expr: Box<Expr> },
/// CONVERT a value to a different data type or character encoding `CONVERT(foo USING utf8mb4)`
Convert {
/// The expression to convert
expr: Box<Expr>,
/// The target data type
data_type: Option<DataType>,
/// The target character encoding
charset: Option<ObjectName>,
/// whether the target comes before the expr (MSSQL syntax)
target_before_value: bool,
},
/// CAST an expression to a different data type e.g. `CAST(foo AS VARCHAR(123))` /// CAST an expression to a different data type e.g. `CAST(foo AS VARCHAR(123))`
Cast { Cast {
expr: Box<Expr>, expr: Box<Expr>,
@ -844,6 +855,28 @@ impl fmt::Display for Expr {
write!(f, "{op}{expr}") write!(f, "{op}{expr}")
} }
} }
Expr::Convert {
expr,
target_before_value,
data_type,
charset,
} => {
write!(f, "CONVERT(")?;
if let Some(data_type) = data_type {
if let Some(charset) = charset {
write!(f, "{expr}, {data_type} CHARACTER SET {charset}")
} else if *target_before_value {
write!(f, "{data_type}, {expr}")
} else {
write!(f, "{expr}, {data_type}")
}
} else if let Some(charset) = charset {
write!(f, "{expr} USING {charset}")
} else {
write!(f, "{expr}") // This should never happen
}?;
write!(f, ")")
}
Expr::Cast { Expr::Cast {
expr, expr,
data_type, data_type,

View file

@ -128,6 +128,11 @@ pub trait Dialect: Debug + Any {
fn supports_in_empty_list(&self) -> bool { fn supports_in_empty_list(&self) -> bool {
false false
} }
/// Returns true if the dialect has a CONVERT function which accepts a type first
/// and an expression second, e.g. `CONVERT(varchar, 1)`
fn convert_type_before_value(&self) -> bool {
false
}
/// Dialect-specific prefix parser override /// Dialect-specific prefix parser override
fn parse_prefix(&self, _parser: &mut Parser) -> Option<Result<Expr, ParserError>> { fn parse_prefix(&self, _parser: &mut Parser) -> Option<Result<Expr, ParserError>> {
// return None to fall back to the default behavior // return None to fall back to the default behavior

View file

@ -35,6 +35,12 @@ impl Dialect for MsSqlDialect {
|| ch == '_' || ch == '_'
} }
/// SQL Server has `CONVERT(type, value)` instead of `CONVERT(value, type)`
/// <https://learn.microsoft.com/en-us/sql/t-sql/functions/cast-and-convert-transact-sql?view=sql-server-ver16>
fn convert_type_before_value(&self) -> bool {
true
}
fn supports_substring_from_for_expr(&self) -> bool { fn supports_substring_from_for_expr(&self) -> bool {
false false
} }

View file

@ -53,4 +53,10 @@ impl Dialect for RedshiftSqlDialect {
// Extends Postgres dialect with sharp // Extends Postgres dialect with sharp
PostgreSqlDialect {}.is_identifier_part(ch) || ch == '#' PostgreSqlDialect {}.is_identifier_part(ch) || ch == '#'
} }
/// redshift has `CONVERT(type, value)` instead of `CONVERT(value, type)`
/// <https://docs.aws.amazon.com/redshift/latest/dg/r_CONVERT_function.html>
fn convert_type_before_value(&self) -> bool {
true
}
} }

View file

@ -821,6 +821,7 @@ impl<'a> Parser<'a> {
self.parse_time_functions(ObjectName(vec![w.to_ident()])) self.parse_time_functions(ObjectName(vec![w.to_ident()]))
} }
Keyword::CASE => self.parse_case_expr(), Keyword::CASE => self.parse_case_expr(),
Keyword::CONVERT => self.parse_convert_expr(),
Keyword::CAST => self.parse_cast_expr(), Keyword::CAST => self.parse_cast_expr(),
Keyword::TRY_CAST => self.parse_try_cast_expr(), Keyword::TRY_CAST => self.parse_try_cast_expr(),
Keyword::SAFE_CAST => self.parse_safe_cast_expr(), Keyword::SAFE_CAST => self.parse_safe_cast_expr(),
@ -1227,6 +1228,57 @@ impl<'a> Parser<'a> {
} }
} }
/// mssql-like convert function
fn parse_mssql_convert(&mut self) -> Result<Expr, ParserError> {
self.expect_token(&Token::LParen)?;
let data_type = self.parse_data_type()?;
self.expect_token(&Token::Comma)?;
let expr = self.parse_expr()?;
self.expect_token(&Token::RParen)?;
Ok(Expr::Convert {
expr: Box::new(expr),
data_type: Some(data_type),
charset: None,
target_before_value: true,
})
}
/// Parse a SQL CONVERT function:
/// - `CONVERT('héhé' USING utf8mb4)` (MySQL)
/// - `CONVERT('héhé', CHAR CHARACTER SET utf8mb4)` (MySQL)
/// - `CONVERT(DECIMAL(10, 5), 42)` (MSSQL) - the type comes first
pub fn parse_convert_expr(&mut self) -> Result<Expr, ParserError> {
if self.dialect.convert_type_before_value() {
return self.parse_mssql_convert();
}
self.expect_token(&Token::LParen)?;
let expr = self.parse_expr()?;
if self.parse_keyword(Keyword::USING) {
let charset = self.parse_object_name()?;
self.expect_token(&Token::RParen)?;
return Ok(Expr::Convert {
expr: Box::new(expr),
data_type: None,
charset: Some(charset),
target_before_value: false,
});
}
self.expect_token(&Token::Comma)?;
let data_type = self.parse_data_type()?;
let charset = if self.parse_keywords(&[Keyword::CHARACTER, Keyword::SET]) {
Some(self.parse_object_name()?)
} else {
None
};
self.expect_token(&Token::RParen)?;
Ok(Expr::Convert {
expr: Box::new(expr),
data_type: Some(data_type),
charset,
target_before_value: false,
})
}
/// Parse a SQL CAST function e.g. `CAST(expr AS FLOAT)` /// Parse a SQL CAST function e.g. `CAST(expr AS FLOAT)`
pub fn parse_cast_expr(&mut self) -> Result<Expr, ParserError> { pub fn parse_cast_expr(&mut self) -> Result<Expr, ParserError> {
self.expect_token(&Token::LParen)?; self.expect_token(&Token::LParen)?;

View file

@ -475,6 +475,13 @@ fn parse_cast_varchar_max() {
ms_and_generic().verified_expr("CAST('foo' AS VARCHAR(MAX))"); ms_and_generic().verified_expr("CAST('foo' AS VARCHAR(MAX))");
} }
#[test]
fn parse_convert() {
ms().verified_expr("CONVERT(VARCHAR(MAX), 'foo')");
ms().verified_expr("CONVERT(VARCHAR(10), 'foo')");
ms().verified_expr("CONVERT(DECIMAL(10,5), 12.55)");
}
#[test] #[test]
fn parse_similar_to() { fn parse_similar_to() {
fn chk(negated: bool) { fn chk(negated: bool) {

View file

@ -1843,3 +1843,18 @@ fn parse_drop_temporary_table() {
_ => unreachable!(), _ => unreachable!(),
} }
} }
#[test]
fn parse_convert_using() {
// https://dev.mysql.com/doc/refman/8.0/en/cast-functions.html#function_convert
// CONVERT(expr USING transcoding_name)
mysql().verified_only_select("SELECT CONVERT('x' USING latin1)");
mysql().verified_only_select("SELECT CONVERT(my_column USING utf8mb4) FROM my_table");
// CONVERT(expr, type)
mysql().verified_only_select("SELECT CONVERT('abc', CHAR(60))");
mysql().verified_only_select("SELECT CONVERT(123.456, DECIMAL(5,2))");
// with a type + a charset
mysql().verified_only_select("SELECT CONVERT('test', CHAR CHARACTER SET utf8mb4)");
}