diff --git a/src/ast/mod.rs b/src/ast/mod.rs index e02741aac..2886704ab 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -492,21 +492,21 @@ pub enum Expr { negated: bool, expr: Box, pattern: Box, - escape_char: Option, + escape_char: Option, }, /// `ILIKE` (case-insensitive `LIKE`) ILike { negated: bool, expr: Box, pattern: Box, - escape_char: Option, + escape_char: Option, }, /// SIMILAR TO regex SimilarTo { negated: bool, expr: Box, pattern: Box, - escape_char: Option, + escape_char: Option, }, /// MySQL: RLIKE regex or REGEXP regex RLike { diff --git a/src/dialect/bigquery.rs b/src/dialect/bigquery.rs index bcd27c3b5..d36910dbc 100644 --- a/src/dialect/bigquery.rs +++ b/src/dialect/bigquery.rs @@ -29,4 +29,9 @@ impl Dialect for BigQueryDialect { fn is_identifier_part(&self, ch: char) -> bool { ch.is_ascii_lowercase() || ch.is_ascii_uppercase() || ch.is_ascii_digit() || ch == '_' } + + // See https://cloud.google.com/bigquery/docs/reference/standard-sql/lexical#escape_sequences + fn supports_string_literal_backslash_escape(&self) -> bool { + true + } } diff --git a/src/dialect/clickhouse.rs b/src/dialect/clickhouse.rs index 50fbde99e..83cc4ae9a 100644 --- a/src/dialect/clickhouse.rs +++ b/src/dialect/clickhouse.rs @@ -25,4 +25,8 @@ impl Dialect for ClickHouseDialect { fn is_identifier_part(&self, ch: char) -> bool { self.is_identifier_start(ch) || ch.is_ascii_digit() } + + fn supports_string_literal_backslash_escape(&self) -> bool { + true + } } diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index 2463121e7..e409c716e 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -120,6 +120,23 @@ pub trait Dialect: Debug + Any { fn is_identifier_start(&self, ch: char) -> bool; /// Determine if a character is a valid unquoted identifier character fn is_identifier_part(&self, ch: char) -> bool; + /// Determine if the dialect supports escaping characters via '\' in string literals. + /// + /// Some dialects like BigQuery and Snowflake support this while others like + /// Postgres do not. Such that the following is accepted by the former but + /// rejected by the latter. + /// ```sql + /// SELECT 'ab\'cd'; + /// ``` + /// + /// Conversely, such dialects reject the following statement which + /// otherwise would be valid in the other dialects. + /// ```sql + /// SELECT '\'; + /// ``` + fn supports_string_literal_backslash_escape(&self) -> bool { + false + } /// Does the dialect support `FILTER (WHERE expr)` for aggregate queries? fn supports_filter_during_aggregation(&self) -> bool { false @@ -306,6 +323,10 @@ mod tests { self.0.identifier_quote_style(identifier) } + fn supports_string_literal_backslash_escape(&self) -> bool { + self.0.supports_string_literal_backslash_escape() + } + fn is_proper_identifier_inside_quotes( &self, chars: std::iter::Peekable>, diff --git a/src/dialect/mysql.rs b/src/dialect/mysql.rs index d0dbe923c..f7711b2b0 100644 --- a/src/dialect/mysql.rs +++ b/src/dialect/mysql.rs @@ -48,6 +48,11 @@ impl Dialect for MySqlDialect { Some('`') } + // See https://dev.mysql.com/doc/refman/8.0/en/string-literals.html#character-escape-sequences + fn supports_string_literal_backslash_escape(&self) -> bool { + true + } + fn parse_infix( &self, parser: &mut crate::parser::Parser, diff --git a/src/dialect/snowflake.rs b/src/dialect/snowflake.rs index 1d9d983e5..28b18b78c 100644 --- a/src/dialect/snowflake.rs +++ b/src/dialect/snowflake.rs @@ -46,6 +46,11 @@ impl Dialect for SnowflakeDialect { || ch == '_' } + // See https://cloud.google.com/bigquery/docs/reference/standard-sql/lexical#escape_sequences + fn supports_string_literal_backslash_escape(&self) -> bool { + true + } + fn supports_within_after_array_aggregation(&self) -> bool { true } diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 5bae7a133..f9bf7968c 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -2584,9 +2584,9 @@ impl<'a> Parser<'a> { } /// parse the ESCAPE CHAR portion of LIKE, ILIKE, and SIMILAR TO - pub fn parse_escape_char(&mut self) -> Result, ParserError> { + pub fn parse_escape_char(&mut self) -> Result, ParserError> { if self.parse_keyword(Keyword::ESCAPE) { - Ok(Some(self.parse_literal_char()?)) + Ok(Some(self.parse_literal_string()?)) } else { Ok(None) } diff --git a/src/tokenizer.rs b/src/tokenizer.rs index b239d990e..b99eeba80 100644 --- a/src/tokenizer.rs +++ b/src/tokenizer.rs @@ -627,11 +627,11 @@ impl<'a> Tokenizer<'a> { chars.next(); // consume match chars.peek() { Some('\'') => { - let s = self.tokenize_quoted_string(chars, '\'')?; + let s = self.tokenize_quoted_string(chars, '\'', false)?; Ok(Some(Token::SingleQuotedByteStringLiteral(s))) } Some('\"') => { - let s = self.tokenize_quoted_string(chars, '\"')?; + let s = self.tokenize_quoted_string(chars, '\"', false)?; Ok(Some(Token::DoubleQuotedByteStringLiteral(s))) } _ => { @@ -646,11 +646,11 @@ impl<'a> Tokenizer<'a> { chars.next(); // consume match chars.peek() { Some('\'') => { - let s = self.tokenize_quoted_string(chars, '\'')?; + let s = self.tokenize_quoted_string(chars, '\'', false)?; Ok(Some(Token::RawStringLiteral(s))) } Some('\"') => { - let s = self.tokenize_quoted_string(chars, '\"')?; + let s = self.tokenize_quoted_string(chars, '\"', false)?; Ok(Some(Token::RawStringLiteral(s))) } _ => { @@ -666,7 +666,7 @@ impl<'a> Tokenizer<'a> { match chars.peek() { Some('\'') => { // N'...' - a - let s = self.tokenize_quoted_string(chars, '\'')?; + let s = self.tokenize_quoted_string(chars, '\'', true)?; Ok(Some(Token::NationalStringLiteral(s))) } _ => { @@ -700,7 +700,7 @@ impl<'a> Tokenizer<'a> { match chars.peek() { Some('\'') => { // X'...' - a - let s = self.tokenize_quoted_string(chars, '\'')?; + let s = self.tokenize_quoted_string(chars, '\'', true)?; Ok(Some(Token::HexStringLiteral(s))) } _ => { @@ -712,7 +712,11 @@ impl<'a> Tokenizer<'a> { } // single quoted string '\'' => { - let s = self.tokenize_quoted_string(chars, '\'')?; + let s = self.tokenize_quoted_string( + chars, + '\'', + self.dialect.supports_string_literal_backslash_escape(), + )?; Ok(Some(Token::SingleQuotedString(s))) } @@ -720,7 +724,11 @@ impl<'a> Tokenizer<'a> { '\"' if !self.dialect.is_delimited_identifier_start(ch) && !self.dialect.is_identifier_start(ch) => { - let s = self.tokenize_quoted_string(chars, '"')?; + let s = self.tokenize_quoted_string( + chars, + '"', + self.dialect.supports_string_literal_backslash_escape(), + )?; Ok(Some(Token::DoubleQuotedString(s))) } @@ -1222,6 +1230,7 @@ impl<'a> Tokenizer<'a> { &self, chars: &mut State, quote_style: char, + allow_escape: bool, ) -> Result { let mut s = String::new(); let error_loc = chars.location(); @@ -1243,35 +1252,31 @@ impl<'a> Tokenizer<'a> { return Ok(s); } } - '\\' => { - // consume + '\\' if allow_escape => { + // consume backslash chars.next(); - // slash escaping is specific to MySQL dialect. - if dialect_of!(self is MySqlDialect) { - if let Some(next) = chars.peek() { - if !self.unescape { - // In no-escape mode, the given query has to be saved completely including backslashes. - s.push(ch); - s.push(*next); - chars.next(); // consume next - } else { - // See https://dev.mysql.com/doc/refman/8.0/en/string-literals.html#character-escape-sequences - let n = match next { - '\'' | '\"' | '\\' | '%' | '_' => *next, - '0' => '\0', - 'b' => '\u{8}', - 'n' => '\n', - 'r' => '\r', - 't' => '\t', - 'Z' => '\u{1a}', - _ => *next, - }; - s.push(n); - chars.next(); // consume next - } + + if let Some(next) = chars.peek() { + if !self.unescape { + // In no-escape mode, the given query has to be saved completely including backslashes. + s.push(ch); + s.push(*next); + chars.next(); // consume next + } else { + let n = match next { + '0' => '\0', + 'a' => '\u{7}', + 'b' => '\u{8}', + 'f' => '\u{c}', + 'n' => '\n', + 'r' => '\r', + 't' => '\t', + 'Z' => '\u{1a}', + _ => *next, + }; + s.push(n); + chars.next(); // consume next } - } else { - s.push(ch); } } _ => { @@ -1517,7 +1522,7 @@ impl<'a: 'b, 'b> Unescape<'a, 'b> { #[cfg(test)] mod tests { use super::*; - use crate::dialect::{ClickHouseDialect, MsSqlDialect}; + use crate::dialect::{BigQueryDialect, ClickHouseDialect, MsSqlDialect}; #[test] fn tokenizer_error_impl() { @@ -2386,4 +2391,57 @@ mod tests { check_unescape(r"Hello\0", None); check_unescape(r"Hello\xCADRust", None); } + + #[test] + fn tokenize_quoted_string_escape() { + for (sql, expected, expected_unescaped) in [ + (r#"'%a\'%b'"#, r#"%a\'%b"#, r#"%a'%b"#), + (r#"'a\'\'b\'c\'d'"#, r#"a\'\'b\'c\'d"#, r#"a''b'c'd"#), + (r#"'\\'"#, r#"\\"#, r#"\"#), + ( + r#"'\0\a\b\f\n\r\t\Z'"#, + r#"\0\a\b\f\n\r\t\Z"#, + "\0\u{7}\u{8}\u{c}\n\r\t\u{1a}", + ), + (r#"'\"'"#, r#"\""#, "\""), + (r#"'\\a\\b\'c'"#, r#"\\a\\b\'c"#, r#"\a\b'c"#), + (r#"'\'abcd'"#, r#"\'abcd"#, r#"'abcd"#), + (r#"'''a''b'"#, r#"''a''b"#, r#"'a'b"#), + ] { + let dialect = BigQueryDialect {}; + + let tokens = Tokenizer::new(&dialect, sql) + .with_unescape(false) + .tokenize() + .unwrap(); + let expected = vec![Token::SingleQuotedString(expected.to_string())]; + compare(expected, tokens); + + let tokens = Tokenizer::new(&dialect, sql) + .with_unescape(true) + .tokenize() + .unwrap(); + let expected = vec![Token::SingleQuotedString(expected_unescaped.to_string())]; + compare(expected, tokens); + } + + for sql in [r#"'\'"#, r#"'ab\'"#] { + let dialect = BigQueryDialect {}; + let mut tokenizer = Tokenizer::new(&dialect, sql); + assert_eq!( + "Unterminated string literal", + tokenizer.tokenize().unwrap_err().message.as_str(), + ); + } + + // Non-escape dialect + for (sql, expected) in [(r#"'\'"#, r#"\"#), (r#"'ab\'"#, r#"ab\"#)] { + let dialect = GenericDialect {}; + let tokens = Tokenizer::new(&dialect, sql).tokenize().unwrap(); + + let expected = vec![Token::SingleQuotedString(expected.to_string())]; + + compare(expected, tokens); + } + } } diff --git a/tests/sqlparser_bigquery.rs b/tests/sqlparser_bigquery.rs index c8f1bb7c1..04f4d7365 100644 --- a/tests/sqlparser_bigquery.rs +++ b/tests/sqlparser_bigquery.rs @@ -1128,115 +1128,6 @@ fn parse_cast_bytes_to_string_format() { bigquery_and_generic().verified_only_select(sql); } -#[test] -fn parse_like() { - fn chk(negated: bool) { - let sql = &format!( - "SELECT * FROM customers WHERE name {}LIKE '%a'", - if negated { "NOT " } else { "" } - ); - let select = bigquery().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 = bigquery().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('\\'), - }, - 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 = bigquery().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 = bigquery().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 = bigquery().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('\\'), - }, - 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 = bigquery().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('\\'), - })), - select.selection.unwrap() - ); - } - chk(false); - chk(true); -} - #[test] fn parse_array_agg_func() { for sql in [ diff --git a/tests/sqlparser_clickhouse.rs b/tests/sqlparser_clickhouse.rs index a3fcc612b..0df9a63e8 100644 --- a/tests/sqlparser_clickhouse.rs +++ b/tests/sqlparser_clickhouse.rs @@ -230,115 +230,6 @@ fn parse_delimited_identifiers() { //TODO verified_stmt(r#"UPDATE foo SET "bar" = 5"#); } -#[test] -fn parse_like() { - fn chk(negated: bool) { - let sql = &format!( - "SELECT * FROM customers WHERE name {}LIKE '%a'", - if negated { "NOT " } else { "" } - ); - let select = clickhouse().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 = clickhouse().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('\\'), - }, - 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 = clickhouse().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 = clickhouse().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 = clickhouse().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('\\'), - }, - 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 = clickhouse().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('\\'), - })), - select.selection.unwrap() - ); - } - chk(false); - chk(true); -} - #[test] fn parse_create_table() { clickhouse().verified_stmt(r#"CREATE TABLE "x" ("a" "int") ENGINE=MergeTree ORDER BY ("x")"#); diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index c94bd3779..d078f7157 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -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) { @@ -8154,6 +8263,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!( diff --git a/tests/sqlparser_hive.rs b/tests/sqlparser_hive.rs index 788c937a6..76fe961fe 100644 --- a/tests/sqlparser_hive.rs +++ b/tests/sqlparser_hive.rs @@ -17,7 +17,7 @@ use sqlparser::ast::{ CreateFunctionBody, CreateFunctionUsing, Expr, Function, FunctionDefinition, Ident, ObjectName, - SelectItem, Statement, TableFactor, UnaryOperator, Value, + SelectItem, Statement, TableFactor, UnaryOperator, }; use sqlparser::dialect::{GenericDialect, HiveDialect, MsSqlDialect}; use sqlparser::parser::{ParserError, ParserOptions}; @@ -420,115 +420,6 @@ fn parse_delimited_identifiers() { //TODO verified_stmt(r#"UPDATE foo SET "bar" = 5"#); } -#[test] -fn parse_like() { - fn chk(negated: bool) { - let sql = &format!( - "SELECT * FROM customers WHERE name {}LIKE '%a'", - if negated { "NOT " } else { "" } - ); - let select = hive().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 = hive().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('\\'), - }, - 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 = hive().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 = hive().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 = hive().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('\\'), - }, - 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 = hive().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('\\'), - })), - select.selection.unwrap() - ); - } - chk(false); - chk(true); -} - fn hive() -> TestedDialects { TestedDialects { dialects: vec![Box::new(HiveDialect {})], diff --git a/tests/sqlparser_mssql.rs b/tests/sqlparser_mssql.rs index ff3e75569..ed4d69e6b 100644 --- a/tests/sqlparser_mssql.rs +++ b/tests/sqlparser_mssql.rs @@ -390,61 +390,6 @@ fn parse_table_name_in_square_brackets() { ); } -#[test] -fn parse_like() { - fn chk(negated: bool) { - let sql = &format!( - "SELECT * FROM customers WHERE name {}LIKE '%a'", - if negated { "NOT " } else { "" } - ); - let select = ms_and_generic().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 = ms_and_generic().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('\\'), - }, - 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 = ms_and_generic().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_for_clause() { ms_and_generic().verified_stmt("SELECT a FROM t FOR JSON PATH"); @@ -495,60 +440,6 @@ fn parse_convert() { ms().verified_expr("CONVERT(DECIMAL(10,5), 12.55)"); } -#[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 = ms_and_generic().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 = ms_and_generic().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('\\'), - }, - 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 = ms_and_generic().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('\\'), - })), - select.selection.unwrap() - ); - } - chk(false); - chk(true); -} - #[test] fn parse_substring_in_select() { let sql = "SELECT DISTINCT SUBSTRING(description, 0, 1) FROM test"; diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index 5f64079a6..9a1d59c9f 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -1061,78 +1061,6 @@ fn parse_unterminated_escape() { assert!(result.is_err()); } -#[test] -fn parse_escaped_string_with_escape() { - fn assert_mysql_query_value(sql: &str, quoted: &str) { - let stmt = TestedDialects { - dialects: vec![Box::new(MySqlDialect {})], - 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 \ '"; - assert_mysql_query_value(sql, "Testing: \0 \\ % _ \u{8} \n \r \t \u{1a} a "); -} - -#[test] -fn parse_escaped_string_with_no_escape() { - fn assert_mysql_query_value(sql: &str, quoted: &str) { - let stmt = TestedDialects { - dialects: vec![Box::new(MySqlDialect {})], - 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 check_roundtrip_of_escaped_string() { let options = Some(ParserOptions::new().with_unescape(false)); diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index ea5c9875b..d7f4f2f84 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -3169,115 +3169,6 @@ fn parse_update_in_with_subquery() { pg_and_generic().verified_stmt(r#"WITH "result" AS (UPDATE "Hero" SET "name" = 'Captain America', "number_of_movies" = "number_of_movies" + 1 WHERE "secret_identity" = 'Sam Wilson' RETURNING "id", "name", "secret_identity", "number_of_movies") SELECT * FROM "result""#); } -#[test] -fn parse_like() { - fn chk(negated: bool) { - let sql = &format!( - "SELECT * FROM customers WHERE name {}LIKE '%a'", - if negated { "NOT " } else { "" } - ); - let select = pg().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 = pg().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('\\'), - }, - 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 = pg().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 = pg().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 = pg().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('\\'), - }, - 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 = pg().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('\\'), - })), - select.selection.unwrap() - ); - } - chk(false); - chk(true); -} - #[test] fn parse_create_function() { let sql = "CREATE FUNCTION add(INTEGER, INTEGER) RETURNS INTEGER LANGUAGE SQL IMMUTABLE STRICT PARALLEL SAFE AS 'select $1 + $2;'"; diff --git a/tests/sqlparser_redshift.rs b/tests/sqlparser_redshift.rs index 6fa647d38..3de229676 100644 --- a/tests/sqlparser_redshift.rs +++ b/tests/sqlparser_redshift.rs @@ -159,115 +159,6 @@ fn parse_delimited_identifiers() { //TODO verified_stmt(r#"UPDATE foo SET "bar" = 5"#); } -#[test] -fn parse_like() { - fn chk(negated: bool) { - let sql = &format!( - "SELECT * FROM customers WHERE name {}LIKE '%a'", - if negated { "NOT " } else { "" } - ); - let select = redshift().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 = redshift().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('\\'), - }, - 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 = redshift().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 = redshift().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 = redshift().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('\\'), - }, - 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 = redshift().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('\\'), - })), - select.selection.unwrap() - ); - } - chk(false); - chk(true); -} - fn redshift() -> TestedDialects { TestedDialects { dialects: vec![Box::new(RedshiftSqlDialect {})], diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index 5c13457b6..7caf3edfa 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -19,7 +19,7 @@ use sqlparser::ast::helpers::stmt_data_loading::{ }; use sqlparser::ast::*; use sqlparser::dialect::{GenericDialect, SnowflakeDialect}; -use sqlparser::parser::ParserError; +use sqlparser::parser::{ParserError, ParserOptions}; use sqlparser::tokenizer::*; use test_utils::*; @@ -308,115 +308,6 @@ fn parse_delimited_identifiers() { //TODO verified_stmt(r#"UPDATE foo SET "bar" = 5"#); } -#[test] -fn parse_like() { - fn chk(negated: bool) { - let sql = &format!( - "SELECT * FROM customers WHERE name {}LIKE '%a'", - if negated { "NOT " } else { "" } - ); - let select = snowflake().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 = snowflake().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('\\'), - }, - 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 = snowflake().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 = snowflake().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 = snowflake().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('\\'), - }, - 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 = snowflake().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('\\'), - })), - select.selection.unwrap() - ); - } - chk(false); - chk(true); -} - #[test] fn test_array_agg_func() { for sql in [ @@ -443,6 +334,13 @@ fn snowflake() -> TestedDialects { } } +fn snowflake_without_unescape() -> TestedDialects { + TestedDialects { + dialects: vec![Box::new(SnowflakeDialect {})], + options: Some(ParserOptions::new().with_unescape(false)), + } +} + fn snowflake_and_generic() -> TestedDialects { TestedDialects { dialects: vec![Box::new(SnowflakeDialect {}), Box::new(GenericDialect {})], @@ -984,10 +882,10 @@ fn test_create_stage_with_file_format() { let sql = concat!( "CREATE OR REPLACE STAGE my_ext_stage ", "URL='s3://load/files/' ", - "FILE_FORMAT=(COMPRESSION=AUTO BINARY_FORMAT=HEX ESCAPE='\\')" + r#"FILE_FORMAT=(COMPRESSION=AUTO BINARY_FORMAT=HEX ESCAPE='\\')"# ); - match snowflake().verified_stmt(sql) { + match snowflake_without_unescape().verified_stmt(sql) { Statement::CreateStage { file_format, .. } => { assert!(file_format.options.contains(&DataLoadingOption { option_name: "COMPRESSION".to_string(), @@ -1002,12 +900,15 @@ fn test_create_stage_with_file_format() { assert!(file_format.options.contains(&DataLoadingOption { option_name: "ESCAPE".to_string(), option_type: DataLoadingOptionType::STRING, - value: "\\".to_string() + value: r#"\\"#.to_string() })); } _ => unreachable!(), }; - assert_eq!(snowflake().verified_stmt(sql).to_string(), sql); + assert_eq!( + snowflake_without_unescape().verified_stmt(sql).to_string(), + sql + ); } #[test] @@ -1242,10 +1143,10 @@ fn test_copy_into_file_format() { "FROM 'gcs://mybucket/./../a.csv' ", "FILES = ('file1.json', 'file2.json') ", "PATTERN = '.*employees0[1-5].csv.gz' ", - "FILE_FORMAT=(COMPRESSION=AUTO BINARY_FORMAT=HEX ESCAPE='\\')" + r#"FILE_FORMAT=(COMPRESSION=AUTO BINARY_FORMAT=HEX ESCAPE='\\')"# ); - match snowflake().verified_stmt(sql) { + match snowflake_without_unescape().verified_stmt(sql) { Statement::CopyIntoSnowflake { file_format, .. } => { assert!(file_format.options.contains(&DataLoadingOption { option_name: "COMPRESSION".to_string(), @@ -1260,12 +1161,15 @@ fn test_copy_into_file_format() { assert!(file_format.options.contains(&DataLoadingOption { option_name: "ESCAPE".to_string(), option_type: DataLoadingOptionType::STRING, - value: "\\".to_string() + value: r#"\\"#.to_string() })); } _ => unreachable!(), } - assert_eq!(snowflake().verified_stmt(sql).to_string(), sql); + assert_eq!( + snowflake_without_unescape().verified_stmt(sql).to_string(), + sql + ); } #[test] diff --git a/tests/sqlparser_sqlite.rs b/tests/sqlparser_sqlite.rs index c9d5d98cd..b90e45827 100644 --- a/tests/sqlparser_sqlite.rs +++ b/tests/sqlparser_sqlite.rs @@ -290,115 +290,6 @@ fn test_placeholder() { ); } -#[test] -fn parse_like() { - fn chk(negated: bool) { - let sql = &format!( - "SELECT * FROM customers WHERE name {}LIKE '%a'", - if negated { "NOT " } else { "" } - ); - let select = sqlite().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 = sqlite().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('\\'), - }, - 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 = sqlite().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 = sqlite().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 = sqlite().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('\\'), - }, - 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 = sqlite().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('\\'), - })), - select.selection.unwrap() - ); - } - chk(false); - chk(true); -} - #[test] fn parse_create_table_with_strict() { let sql = "CREATE TABLE Fruits (id TEXT NOT NULL PRIMARY KEY) STRICT";