From 0458e4b0fcedcb057a674a351b59ada534b6d6c0 Mon Sep 17 00:00:00 2001 From: ifeanyi Date: Wed, 13 Mar 2024 21:36:27 +0100 Subject: [PATCH] Add support backslash escape This adds support for parsing string literals on dialects that treat backslash character as an escape character. As an example, the following previously failed to parse by dialects like BigQuery where the syntax is valid. ```sql SELECT 'a\'b'; ``` Moves the SQL `like` and `similar_to` tests from individual dialects to common since the tests were identical. --- src/ast/mod.rs | 6 +- src/dialect/bigquery.rs | 5 + src/dialect/clickhouse.rs | 4 + src/dialect/mod.rs | 21 ++++ src/dialect/mysql.rs | 5 + src/dialect/snowflake.rs | 5 + src/parser/mod.rs | 4 +- src/tokenizer.rs | 130 ++++++++++++++++------- tests/sqlparser_bigquery.rs | 109 ------------------- tests/sqlparser_clickhouse.rs | 109 ------------------- tests/sqlparser_common.rs | 191 +++++++++++++++++++++++++++++++++- tests/sqlparser_hive.rs | 111 +------------------- tests/sqlparser_mssql.rs | 109 ------------------- tests/sqlparser_mysql.rs | 72 ------------- tests/sqlparser_postgres.rs | 109 ------------------- tests/sqlparser_redshift.rs | 109 ------------------- tests/sqlparser_snowflake.rs | 140 ++++--------------------- tests/sqlparser_sqlite.rs | 109 ------------------- 18 files changed, 352 insertions(+), 996 deletions(-) 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";