mirror of
https://github.com/apache/datafusion-sqlparser-rs.git
synced 2025-08-30 18:57:21 +00:00
Add support for quoted string backslash escaping (#1177)
This commit is contained in:
parent
7b49c69b3a
commit
d2c2b15f9e
18 changed files with 352 additions and 996 deletions
|
@ -512,21 +512,21 @@ pub enum Expr {
|
||||||
negated: bool,
|
negated: bool,
|
||||||
expr: Box<Expr>,
|
expr: Box<Expr>,
|
||||||
pattern: Box<Expr>,
|
pattern: Box<Expr>,
|
||||||
escape_char: Option<char>,
|
escape_char: Option<String>,
|
||||||
},
|
},
|
||||||
/// `ILIKE` (case-insensitive `LIKE`)
|
/// `ILIKE` (case-insensitive `LIKE`)
|
||||||
ILike {
|
ILike {
|
||||||
negated: bool,
|
negated: bool,
|
||||||
expr: Box<Expr>,
|
expr: Box<Expr>,
|
||||||
pattern: Box<Expr>,
|
pattern: Box<Expr>,
|
||||||
escape_char: Option<char>,
|
escape_char: Option<String>,
|
||||||
},
|
},
|
||||||
/// SIMILAR TO regex
|
/// SIMILAR TO regex
|
||||||
SimilarTo {
|
SimilarTo {
|
||||||
negated: bool,
|
negated: bool,
|
||||||
expr: Box<Expr>,
|
expr: Box<Expr>,
|
||||||
pattern: Box<Expr>,
|
pattern: Box<Expr>,
|
||||||
escape_char: Option<char>,
|
escape_char: Option<String>,
|
||||||
},
|
},
|
||||||
/// MySQL: RLIKE regex or REGEXP regex
|
/// MySQL: RLIKE regex or REGEXP regex
|
||||||
RLike {
|
RLike {
|
||||||
|
|
|
@ -29,4 +29,9 @@ impl Dialect for BigQueryDialect {
|
||||||
fn is_identifier_part(&self, ch: char) -> bool {
|
fn is_identifier_part(&self, ch: char) -> bool {
|
||||||
ch.is_ascii_lowercase() || ch.is_ascii_uppercase() || ch.is_ascii_digit() || ch == '_'
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,4 +25,8 @@ impl Dialect for ClickHouseDialect {
|
||||||
fn is_identifier_part(&self, ch: char) -> bool {
|
fn is_identifier_part(&self, ch: char) -> bool {
|
||||||
self.is_identifier_start(ch) || ch.is_ascii_digit()
|
self.is_identifier_start(ch) || ch.is_ascii_digit()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn supports_string_literal_backslash_escape(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -120,6 +120,23 @@ pub trait Dialect: Debug + Any {
|
||||||
fn is_identifier_start(&self, ch: char) -> bool;
|
fn is_identifier_start(&self, ch: char) -> bool;
|
||||||
/// Determine if a character is a valid unquoted identifier character
|
/// Determine if a character is a valid unquoted identifier character
|
||||||
fn is_identifier_part(&self, ch: char) -> bool;
|
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?
|
/// Does the dialect support `FILTER (WHERE expr)` for aggregate queries?
|
||||||
fn supports_filter_during_aggregation(&self) -> bool {
|
fn supports_filter_during_aggregation(&self) -> bool {
|
||||||
false
|
false
|
||||||
|
@ -306,6 +323,10 @@ mod tests {
|
||||||
self.0.identifier_quote_style(identifier)
|
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(
|
fn is_proper_identifier_inside_quotes(
|
||||||
&self,
|
&self,
|
||||||
chars: std::iter::Peekable<std::str::Chars<'_>>,
|
chars: std::iter::Peekable<std::str::Chars<'_>>,
|
||||||
|
|
|
@ -48,6 +48,11 @@ impl Dialect for MySqlDialect {
|
||||||
Some('`')
|
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(
|
fn parse_infix(
|
||||||
&self,
|
&self,
|
||||||
parser: &mut crate::parser::Parser,
|
parser: &mut crate::parser::Parser,
|
||||||
|
|
|
@ -46,6 +46,11 @@ impl Dialect for SnowflakeDialect {
|
||||||
|| ch == '_'
|
|| 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 {
|
fn supports_within_after_array_aggregation(&self) -> bool {
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
|
@ -2560,9 +2560,9 @@ impl<'a> Parser<'a> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// parse the ESCAPE CHAR portion of LIKE, ILIKE, and SIMILAR TO
|
/// parse the ESCAPE CHAR portion of LIKE, ILIKE, and SIMILAR TO
|
||||||
pub fn parse_escape_char(&mut self) -> Result<Option<char>, ParserError> {
|
pub fn parse_escape_char(&mut self) -> Result<Option<String>, ParserError> {
|
||||||
if self.parse_keyword(Keyword::ESCAPE) {
|
if self.parse_keyword(Keyword::ESCAPE) {
|
||||||
Ok(Some(self.parse_literal_char()?))
|
Ok(Some(self.parse_literal_string()?))
|
||||||
} else {
|
} else {
|
||||||
Ok(None)
|
Ok(None)
|
||||||
}
|
}
|
||||||
|
|
|
@ -627,11 +627,11 @@ impl<'a> Tokenizer<'a> {
|
||||||
chars.next(); // consume
|
chars.next(); // consume
|
||||||
match chars.peek() {
|
match chars.peek() {
|
||||||
Some('\'') => {
|
Some('\'') => {
|
||||||
let s = self.tokenize_quoted_string(chars, '\'')?;
|
let s = self.tokenize_quoted_string(chars, '\'', false)?;
|
||||||
Ok(Some(Token::SingleQuotedByteStringLiteral(s)))
|
Ok(Some(Token::SingleQuotedByteStringLiteral(s)))
|
||||||
}
|
}
|
||||||
Some('\"') => {
|
Some('\"') => {
|
||||||
let s = self.tokenize_quoted_string(chars, '\"')?;
|
let s = self.tokenize_quoted_string(chars, '\"', false)?;
|
||||||
Ok(Some(Token::DoubleQuotedByteStringLiteral(s)))
|
Ok(Some(Token::DoubleQuotedByteStringLiteral(s)))
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
|
@ -646,11 +646,11 @@ impl<'a> Tokenizer<'a> {
|
||||||
chars.next(); // consume
|
chars.next(); // consume
|
||||||
match chars.peek() {
|
match chars.peek() {
|
||||||
Some('\'') => {
|
Some('\'') => {
|
||||||
let s = self.tokenize_quoted_string(chars, '\'')?;
|
let s = self.tokenize_quoted_string(chars, '\'', false)?;
|
||||||
Ok(Some(Token::RawStringLiteral(s)))
|
Ok(Some(Token::RawStringLiteral(s)))
|
||||||
}
|
}
|
||||||
Some('\"') => {
|
Some('\"') => {
|
||||||
let s = self.tokenize_quoted_string(chars, '\"')?;
|
let s = self.tokenize_quoted_string(chars, '\"', false)?;
|
||||||
Ok(Some(Token::RawStringLiteral(s)))
|
Ok(Some(Token::RawStringLiteral(s)))
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
|
@ -666,7 +666,7 @@ impl<'a> Tokenizer<'a> {
|
||||||
match chars.peek() {
|
match chars.peek() {
|
||||||
Some('\'') => {
|
Some('\'') => {
|
||||||
// N'...' - a <national character string literal>
|
// N'...' - a <national character string literal>
|
||||||
let s = self.tokenize_quoted_string(chars, '\'')?;
|
let s = self.tokenize_quoted_string(chars, '\'', true)?;
|
||||||
Ok(Some(Token::NationalStringLiteral(s)))
|
Ok(Some(Token::NationalStringLiteral(s)))
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
|
@ -700,7 +700,7 @@ impl<'a> Tokenizer<'a> {
|
||||||
match chars.peek() {
|
match chars.peek() {
|
||||||
Some('\'') => {
|
Some('\'') => {
|
||||||
// X'...' - a <binary string literal>
|
// X'...' - a <binary string literal>
|
||||||
let s = self.tokenize_quoted_string(chars, '\'')?;
|
let s = self.tokenize_quoted_string(chars, '\'', true)?;
|
||||||
Ok(Some(Token::HexStringLiteral(s)))
|
Ok(Some(Token::HexStringLiteral(s)))
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
|
@ -712,7 +712,11 @@ impl<'a> Tokenizer<'a> {
|
||||||
}
|
}
|
||||||
// single quoted string
|
// 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)))
|
Ok(Some(Token::SingleQuotedString(s)))
|
||||||
}
|
}
|
||||||
|
@ -720,7 +724,11 @@ impl<'a> Tokenizer<'a> {
|
||||||
'\"' if !self.dialect.is_delimited_identifier_start(ch)
|
'\"' if !self.dialect.is_delimited_identifier_start(ch)
|
||||||
&& !self.dialect.is_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)))
|
Ok(Some(Token::DoubleQuotedString(s)))
|
||||||
}
|
}
|
||||||
|
@ -1222,6 +1230,7 @@ impl<'a> Tokenizer<'a> {
|
||||||
&self,
|
&self,
|
||||||
chars: &mut State,
|
chars: &mut State,
|
||||||
quote_style: char,
|
quote_style: char,
|
||||||
|
allow_escape: bool,
|
||||||
) -> Result<String, TokenizerError> {
|
) -> Result<String, TokenizerError> {
|
||||||
let mut s = String::new();
|
let mut s = String::new();
|
||||||
let error_loc = chars.location();
|
let error_loc = chars.location();
|
||||||
|
@ -1243,11 +1252,10 @@ impl<'a> Tokenizer<'a> {
|
||||||
return Ok(s);
|
return Ok(s);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
'\\' => {
|
'\\' if allow_escape => {
|
||||||
// consume
|
// consume backslash
|
||||||
chars.next();
|
chars.next();
|
||||||
// slash escaping is specific to MySQL dialect.
|
|
||||||
if dialect_of!(self is MySqlDialect) {
|
|
||||||
if let Some(next) = chars.peek() {
|
if let Some(next) = chars.peek() {
|
||||||
if !self.unescape {
|
if !self.unescape {
|
||||||
// In no-escape mode, the given query has to be saved completely including backslashes.
|
// In no-escape mode, the given query has to be saved completely including backslashes.
|
||||||
|
@ -1255,11 +1263,11 @@ impl<'a> Tokenizer<'a> {
|
||||||
s.push(*next);
|
s.push(*next);
|
||||||
chars.next(); // consume next
|
chars.next(); // consume next
|
||||||
} else {
|
} else {
|
||||||
// See https://dev.mysql.com/doc/refman/8.0/en/string-literals.html#character-escape-sequences
|
|
||||||
let n = match next {
|
let n = match next {
|
||||||
'\'' | '\"' | '\\' | '%' | '_' => *next,
|
|
||||||
'0' => '\0',
|
'0' => '\0',
|
||||||
|
'a' => '\u{7}',
|
||||||
'b' => '\u{8}',
|
'b' => '\u{8}',
|
||||||
|
'f' => '\u{c}',
|
||||||
'n' => '\n',
|
'n' => '\n',
|
||||||
'r' => '\r',
|
'r' => '\r',
|
||||||
't' => '\t',
|
't' => '\t',
|
||||||
|
@ -1270,9 +1278,6 @@ impl<'a> Tokenizer<'a> {
|
||||||
chars.next(); // consume next
|
chars.next(); // consume next
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
s.push(ch);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
chars.next(); // consume
|
chars.next(); // consume
|
||||||
|
@ -1517,7 +1522,7 @@ impl<'a: 'b, 'b> Unescape<'a, 'b> {
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::dialect::{ClickHouseDialect, MsSqlDialect};
|
use crate::dialect::{BigQueryDialect, ClickHouseDialect, MsSqlDialect};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn tokenizer_error_impl() {
|
fn tokenizer_error_impl() {
|
||||||
|
@ -2386,4 +2391,57 @@ mod tests {
|
||||||
check_unescape(r"Hello\0", None);
|
check_unescape(r"Hello\0", None);
|
||||||
check_unescape(r"Hello\xCADRust", 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1128,115 +1128,6 @@ fn parse_cast_bytes_to_string_format() {
|
||||||
bigquery_and_generic().verified_only_select(sql);
|
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]
|
#[test]
|
||||||
fn parse_array_agg_func() {
|
fn parse_array_agg_func() {
|
||||||
for sql in [
|
for sql in [
|
||||||
|
|
|
@ -230,115 +230,6 @@ fn parse_delimited_identifiers() {
|
||||||
//TODO verified_stmt(r#"UPDATE foo SET "bar" = 5"#);
|
//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]
|
#[test]
|
||||||
fn parse_create_table() {
|
fn parse_create_table() {
|
||||||
clickhouse().verified_stmt(r#"CREATE TABLE "x" ("a" "int") ENGINE=MergeTree ORDER BY ("x")"#);
|
clickhouse().verified_stmt(r#"CREATE TABLE "x" ("a" "int") ENGINE=MergeTree ORDER BY ("x")"#);
|
||||||
|
|
|
@ -1599,7 +1599,7 @@ fn parse_ilike() {
|
||||||
expr: Box::new(Expr::Identifier(Ident::new("name"))),
|
expr: Box::new(Expr::Identifier(Ident::new("name"))),
|
||||||
negated,
|
negated,
|
||||||
pattern: Box::new(Expr::Value(Value::SingleQuotedString("%a".to_string()))),
|
pattern: Box::new(Expr::Value(Value::SingleQuotedString("%a".to_string()))),
|
||||||
escape_char: Some('^'),
|
escape_char: Some('^'.to_string()),
|
||||||
},
|
},
|
||||||
select.selection.unwrap()
|
select.selection.unwrap()
|
||||||
);
|
);
|
||||||
|
@ -1625,6 +1625,115 @@ fn parse_ilike() {
|
||||||
chk(true);
|
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]
|
#[test]
|
||||||
fn parse_in_list() {
|
fn parse_in_list() {
|
||||||
fn chk(negated: bool) {
|
fn chk(negated: bool) {
|
||||||
|
@ -8166,6 +8275,86 @@ fn parse_with_recursion_limit() {
|
||||||
assert!(res.is_ok(), "{res:?}");
|
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]
|
#[test]
|
||||||
fn parse_pivot_table() {
|
fn parse_pivot_table() {
|
||||||
let sql = concat!(
|
let sql = concat!(
|
||||||
|
|
|
@ -17,7 +17,7 @@
|
||||||
|
|
||||||
use sqlparser::ast::{
|
use sqlparser::ast::{
|
||||||
CreateFunctionBody, CreateFunctionUsing, Expr, Function, FunctionDefinition, Ident, ObjectName,
|
CreateFunctionBody, CreateFunctionUsing, Expr, Function, FunctionDefinition, Ident, ObjectName,
|
||||||
SelectItem, Statement, TableFactor, UnaryOperator, Value,
|
SelectItem, Statement, TableFactor, UnaryOperator,
|
||||||
};
|
};
|
||||||
use sqlparser::dialect::{GenericDialect, HiveDialect, MsSqlDialect};
|
use sqlparser::dialect::{GenericDialect, HiveDialect, MsSqlDialect};
|
||||||
use sqlparser::parser::{ParserError, ParserOptions};
|
use sqlparser::parser::{ParserError, ParserOptions};
|
||||||
|
@ -420,115 +420,6 @@ fn parse_delimited_identifiers() {
|
||||||
//TODO verified_stmt(r#"UPDATE foo SET "bar" = 5"#);
|
//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 {
|
fn hive() -> TestedDialects {
|
||||||
TestedDialects {
|
TestedDialects {
|
||||||
dialects: vec![Box::new(HiveDialect {})],
|
dialects: vec![Box::new(HiveDialect {})],
|
||||||
|
|
|
@ -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]
|
#[test]
|
||||||
fn parse_for_clause() {
|
fn parse_for_clause() {
|
||||||
ms_and_generic().verified_stmt("SELECT a FROM t FOR JSON PATH");
|
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)");
|
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]
|
#[test]
|
||||||
fn parse_substring_in_select() {
|
fn parse_substring_in_select() {
|
||||||
let sql = "SELECT DISTINCT SUBSTRING(description, 0, 1) FROM test";
|
let sql = "SELECT DISTINCT SUBSTRING(description, 0, 1) FROM test";
|
||||||
|
|
|
@ -1061,78 +1061,6 @@ fn parse_unterminated_escape() {
|
||||||
assert!(result.is_err());
|
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]
|
#[test]
|
||||||
fn check_roundtrip_of_escaped_string() {
|
fn check_roundtrip_of_escaped_string() {
|
||||||
let options = Some(ParserOptions::new().with_unescape(false));
|
let options = Some(ParserOptions::new().with_unescape(false));
|
||||||
|
|
|
@ -3171,115 +3171,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""#);
|
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]
|
#[test]
|
||||||
fn parse_create_function() {
|
fn parse_create_function() {
|
||||||
let sql = "CREATE FUNCTION add(INTEGER, INTEGER) RETURNS INTEGER LANGUAGE SQL IMMUTABLE STRICT PARALLEL SAFE AS 'select $1 + $2;'";
|
let sql = "CREATE FUNCTION add(INTEGER, INTEGER) RETURNS INTEGER LANGUAGE SQL IMMUTABLE STRICT PARALLEL SAFE AS 'select $1 + $2;'";
|
||||||
|
|
|
@ -159,115 +159,6 @@ fn parse_delimited_identifiers() {
|
||||||
//TODO verified_stmt(r#"UPDATE foo SET "bar" = 5"#);
|
//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 {
|
fn redshift() -> TestedDialects {
|
||||||
TestedDialects {
|
TestedDialects {
|
||||||
dialects: vec![Box::new(RedshiftSqlDialect {})],
|
dialects: vec![Box::new(RedshiftSqlDialect {})],
|
||||||
|
|
|
@ -19,7 +19,7 @@ use sqlparser::ast::helpers::stmt_data_loading::{
|
||||||
};
|
};
|
||||||
use sqlparser::ast::*;
|
use sqlparser::ast::*;
|
||||||
use sqlparser::dialect::{GenericDialect, SnowflakeDialect};
|
use sqlparser::dialect::{GenericDialect, SnowflakeDialect};
|
||||||
use sqlparser::parser::ParserError;
|
use sqlparser::parser::{ParserError, ParserOptions};
|
||||||
use sqlparser::tokenizer::*;
|
use sqlparser::tokenizer::*;
|
||||||
use test_utils::*;
|
use test_utils::*;
|
||||||
|
|
||||||
|
@ -309,115 +309,6 @@ fn parse_delimited_identifiers() {
|
||||||
//TODO verified_stmt(r#"UPDATE foo SET "bar" = 5"#);
|
//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]
|
#[test]
|
||||||
fn test_array_agg_func() {
|
fn test_array_agg_func() {
|
||||||
for sql in [
|
for sql in [
|
||||||
|
@ -444,6 +335,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 {
|
fn snowflake_and_generic() -> TestedDialects {
|
||||||
TestedDialects {
|
TestedDialects {
|
||||||
dialects: vec![Box::new(SnowflakeDialect {}), Box::new(GenericDialect {})],
|
dialects: vec![Box::new(SnowflakeDialect {}), Box::new(GenericDialect {})],
|
||||||
|
@ -985,10 +883,10 @@ fn test_create_stage_with_file_format() {
|
||||||
let sql = concat!(
|
let sql = concat!(
|
||||||
"CREATE OR REPLACE STAGE my_ext_stage ",
|
"CREATE OR REPLACE STAGE my_ext_stage ",
|
||||||
"URL='s3://load/files/' ",
|
"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, .. } => {
|
Statement::CreateStage { file_format, .. } => {
|
||||||
assert!(file_format.options.contains(&DataLoadingOption {
|
assert!(file_format.options.contains(&DataLoadingOption {
|
||||||
option_name: "COMPRESSION".to_string(),
|
option_name: "COMPRESSION".to_string(),
|
||||||
|
@ -1003,12 +901,15 @@ fn test_create_stage_with_file_format() {
|
||||||
assert!(file_format.options.contains(&DataLoadingOption {
|
assert!(file_format.options.contains(&DataLoadingOption {
|
||||||
option_name: "ESCAPE".to_string(),
|
option_name: "ESCAPE".to_string(),
|
||||||
option_type: DataLoadingOptionType::STRING,
|
option_type: DataLoadingOptionType::STRING,
|
||||||
value: "\\".to_string()
|
value: r#"\\"#.to_string()
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
_ => unreachable!(),
|
_ => unreachable!(),
|
||||||
};
|
};
|
||||||
assert_eq!(snowflake().verified_stmt(sql).to_string(), sql);
|
assert_eq!(
|
||||||
|
snowflake_without_unescape().verified_stmt(sql).to_string(),
|
||||||
|
sql
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -1243,10 +1144,10 @@ fn test_copy_into_file_format() {
|
||||||
"FROM 'gcs://mybucket/./../a.csv' ",
|
"FROM 'gcs://mybucket/./../a.csv' ",
|
||||||
"FILES = ('file1.json', 'file2.json') ",
|
"FILES = ('file1.json', 'file2.json') ",
|
||||||
"PATTERN = '.*employees0[1-5].csv.gz' ",
|
"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, .. } => {
|
Statement::CopyIntoSnowflake { file_format, .. } => {
|
||||||
assert!(file_format.options.contains(&DataLoadingOption {
|
assert!(file_format.options.contains(&DataLoadingOption {
|
||||||
option_name: "COMPRESSION".to_string(),
|
option_name: "COMPRESSION".to_string(),
|
||||||
|
@ -1261,12 +1162,15 @@ fn test_copy_into_file_format() {
|
||||||
assert!(file_format.options.contains(&DataLoadingOption {
|
assert!(file_format.options.contains(&DataLoadingOption {
|
||||||
option_name: "ESCAPE".to_string(),
|
option_name: "ESCAPE".to_string(),
|
||||||
option_type: DataLoadingOptionType::STRING,
|
option_type: DataLoadingOptionType::STRING,
|
||||||
value: "\\".to_string()
|
value: r#"\\"#.to_string()
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
_ => unreachable!(),
|
_ => unreachable!(),
|
||||||
}
|
}
|
||||||
assert_eq!(snowflake().verified_stmt(sql).to_string(), sql);
|
assert_eq!(
|
||||||
|
snowflake_without_unescape().verified_stmt(sql).to_string(),
|
||||||
|
sql
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
|
@ -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]
|
#[test]
|
||||||
fn parse_create_table_with_strict() {
|
fn parse_create_table_with_strict() {
|
||||||
let sql = "CREATE TABLE Fruits (id TEXT NOT NULL PRIMARY KEY) STRICT";
|
let sql = "CREATE TABLE Fruits (id TEXT NOT NULL PRIMARY KEY) STRICT";
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue