Add support for quoted string backslash escaping (#1177)

This commit is contained in:
Ifeanyi Ubah 2024-04-21 15:07:56 +02:00 committed by GitHub
parent 7b49c69b3a
commit d2c2b15f9e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 352 additions and 996 deletions

View file

@ -1599,7 +1599,7 @@ fn parse_ilike() {
expr: Box::new(Expr::Identifier(Ident::new("name"))),
negated,
pattern: Box::new(Expr::Value(Value::SingleQuotedString("%a".to_string()))),
escape_char: Some('^'),
escape_char: Some('^'.to_string()),
},
select.selection.unwrap()
);
@ -1625,6 +1625,115 @@ fn parse_ilike() {
chk(true);
}
#[test]
fn parse_like() {
fn chk(negated: bool) {
let sql = &format!(
"SELECT * FROM customers WHERE name {}LIKE '%a'",
if negated { "NOT " } else { "" }
);
let select = verified_only_select(sql);
assert_eq!(
Expr::Like {
expr: Box::new(Expr::Identifier(Ident::new("name"))),
negated,
pattern: Box::new(Expr::Value(Value::SingleQuotedString("%a".to_string()))),
escape_char: None,
},
select.selection.unwrap()
);
// Test with escape char
let sql = &format!(
"SELECT * FROM customers WHERE name {}LIKE '%a' ESCAPE '^'",
if negated { "NOT " } else { "" }
);
let select = verified_only_select(sql);
assert_eq!(
Expr::Like {
expr: Box::new(Expr::Identifier(Ident::new("name"))),
negated,
pattern: Box::new(Expr::Value(Value::SingleQuotedString("%a".to_string()))),
escape_char: Some('^'.to_string()),
},
select.selection.unwrap()
);
// This statement tests that LIKE and NOT LIKE have the same precedence.
// This was previously mishandled (#81).
let sql = &format!(
"SELECT * FROM customers WHERE name {}LIKE '%a' IS NULL",
if negated { "NOT " } else { "" }
);
let select = verified_only_select(sql);
assert_eq!(
Expr::IsNull(Box::new(Expr::Like {
expr: Box::new(Expr::Identifier(Ident::new("name"))),
negated,
pattern: Box::new(Expr::Value(Value::SingleQuotedString("%a".to_string()))),
escape_char: None,
})),
select.selection.unwrap()
);
}
chk(false);
chk(true);
}
#[test]
fn parse_similar_to() {
fn chk(negated: bool) {
let sql = &format!(
"SELECT * FROM customers WHERE name {}SIMILAR TO '%a'",
if negated { "NOT " } else { "" }
);
let select = verified_only_select(sql);
assert_eq!(
Expr::SimilarTo {
expr: Box::new(Expr::Identifier(Ident::new("name"))),
negated,
pattern: Box::new(Expr::Value(Value::SingleQuotedString("%a".to_string()))),
escape_char: None,
},
select.selection.unwrap()
);
// Test with escape char
let sql = &format!(
"SELECT * FROM customers WHERE name {}SIMILAR TO '%a' ESCAPE '^'",
if negated { "NOT " } else { "" }
);
let select = verified_only_select(sql);
assert_eq!(
Expr::SimilarTo {
expr: Box::new(Expr::Identifier(Ident::new("name"))),
negated,
pattern: Box::new(Expr::Value(Value::SingleQuotedString("%a".to_string()))),
escape_char: Some('^'.to_string()),
},
select.selection.unwrap()
);
// This statement tests that SIMILAR TO and NOT SIMILAR TO have the same precedence.
let sql = &format!(
"SELECT * FROM customers WHERE name {}SIMILAR TO '%a' ESCAPE '^' IS NULL",
if negated { "NOT " } else { "" }
);
let select = verified_only_select(sql);
assert_eq!(
Expr::IsNull(Box::new(Expr::SimilarTo {
expr: Box::new(Expr::Identifier(Ident::new("name"))),
negated,
pattern: Box::new(Expr::Value(Value::SingleQuotedString("%a".to_string()))),
escape_char: Some('^'.to_string()),
})),
select.selection.unwrap()
);
}
chk(false);
chk(true);
}
#[test]
fn parse_in_list() {
fn chk(negated: bool) {
@ -8166,6 +8275,86 @@ fn parse_with_recursion_limit() {
assert!(res.is_ok(), "{res:?}");
}
#[test]
fn parse_escaped_string_with_unescape() {
fn assert_mysql_query_value(sql: &str, quoted: &str) {
let stmt = TestedDialects {
dialects: vec![
Box::new(MySqlDialect {}),
Box::new(BigQueryDialect {}),
Box::new(SnowflakeDialect {}),
],
options: None,
}
.one_statement_parses_to(sql, "");
match stmt {
Statement::Query(query) => match *query.body {
SetExpr::Select(value) => {
let expr = expr_from_projection(only(&value.projection));
assert_eq!(
*expr,
Expr::Value(Value::SingleQuotedString(quoted.to_string()))
);
}
_ => unreachable!(),
},
_ => unreachable!(),
};
}
let sql = r"SELECT 'I\'m fine'";
assert_mysql_query_value(sql, "I'm fine");
let sql = r#"SELECT 'I''m fine'"#;
assert_mysql_query_value(sql, "I'm fine");
let sql = r#"SELECT 'I\"m fine'"#;
assert_mysql_query_value(sql, "I\"m fine");
let sql = r"SELECT 'Testing: \0 \\ \% \_ \b \n \r \t \Z \a \h \ '";
assert_mysql_query_value(sql, "Testing: \0 \\ % _ \u{8} \n \r \t \u{1a} \u{7} h ");
}
#[test]
fn parse_escaped_string_without_unescape() {
fn assert_mysql_query_value(sql: &str, quoted: &str) {
let stmt = TestedDialects {
dialects: vec![
Box::new(MySqlDialect {}),
Box::new(BigQueryDialect {}),
Box::new(SnowflakeDialect {}),
],
options: Some(ParserOptions::new().with_unescape(false)),
}
.one_statement_parses_to(sql, "");
match stmt {
Statement::Query(query) => match *query.body {
SetExpr::Select(value) => {
let expr = expr_from_projection(only(&value.projection));
assert_eq!(
*expr,
Expr::Value(Value::SingleQuotedString(quoted.to_string()))
);
}
_ => unreachable!(),
},
_ => unreachable!(),
};
}
let sql = r"SELECT 'I\'m fine'";
assert_mysql_query_value(sql, r"I\'m fine");
let sql = r#"SELECT 'I''m fine'"#;
assert_mysql_query_value(sql, r#"I''m fine"#);
let sql = r#"SELECT 'I\"m fine'"#;
assert_mysql_query_value(sql, r#"I\"m fine"#);
let sql = r"SELECT 'Testing: \0 \\ \% \_ \b \n \r \t \Z \a \ '";
assert_mysql_query_value(sql, r"Testing: \0 \\ \% \_ \b \n \r \t \Z \a \ ");
}
#[test]
fn parse_pivot_table() {
let sql = concat!(