From 5aa4d9a8ca4b5ebc17c73ffca9ba4b6e42f62281 Mon Sep 17 00:00:00 2001 From: Hiranmaya Gundu Date: Fri, 19 Apr 2024 16:02:20 -0700 Subject: [PATCH 1/7] feat: implement wildcard select ilike --- src/ast/mod.rs | 4 ++-- src/ast/query.rs | 25 +++++++++++++++++++++++++ src/parser/mod.rs | 26 +++++++++++++++++++++++++- tests/sqlparser_common.rs | 1 + tests/sqlparser_duckdb.rs | 2 ++ tests/sqlparser_snowflake.rs | 21 +++++++++++++++++++++ 6 files changed, 76 insertions(+), 3 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index acc294c09..2a4e60892 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -40,8 +40,8 @@ pub use self::ddl::{ pub use self::operator::{BinaryOperator, UnaryOperator}; pub use self::query::{ ConnectBy, Cte, CteAsMaterialized, Distinct, ExceptSelectItem, ExcludeSelectItem, Fetch, - ForClause, ForJson, ForXml, GroupByExpr, IdentWithAlias, Join, JoinConstraint, JoinOperator, - JsonTableColumn, JsonTableColumnErrorHandling, LateralView, LockClause, LockType, + ForClause, ForJson, ForXml, GroupByExpr, IdentWithAlias, IlikeSelectItem, Join, JoinConstraint, + JoinOperator, JsonTableColumn, JsonTableColumnErrorHandling, LateralView, LockClause, LockType, NamedWindowDefinition, NonBlock, Offset, OffsetRows, OrderByExpr, Query, RenameSelectItem, ReplaceSelectElement, ReplaceSelectItem, Select, SelectInto, SelectItem, SetExpr, SetOperator, SetQuantifier, Table, TableAlias, TableFactor, TableVersion, TableWithJoins, Top, TopQuantity, diff --git a/src/ast/query.rs b/src/ast/query.rs index 04309d9b7..34ad74da6 100644 --- a/src/ast/query.rs +++ b/src/ast/query.rs @@ -477,6 +477,9 @@ impl fmt::Display for IdentWithAlias { #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] pub struct WildcardAdditionalOptions { + /// `[ILIKE...]`. + /// Snowflake syntax: + pub opt_ilike: Option, /// `[EXCLUDE...]`. pub opt_exclude: Option, /// `[EXCEPT...]`. @@ -492,6 +495,9 @@ pub struct WildcardAdditionalOptions { impl fmt::Display for WildcardAdditionalOptions { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + if let Some(ilike) = &self.opt_ilike { + write!(f, " {ilike}")?; + } if let Some(exclude) = &self.opt_exclude { write!(f, " {exclude}")?; } @@ -508,6 +514,25 @@ impl fmt::Display for WildcardAdditionalOptions { } } +/// Snowflake `ILIKE` information. +/// +/// # Syntax +/// ```plaintext +/// ILIKE +/// ``` +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct IlikeSelectItem { + pub pattern: Expr, +} + +impl fmt::Display for IlikeSelectItem { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "ILIKE {}", self.pattern)?; + Ok(()) + } +} /// Snowflake `EXCLUDE` information. /// /// # Syntax diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 8ddff71fe..0515b1b60 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -8775,9 +8775,14 @@ impl<'a> Parser<'a> { pub fn parse_wildcard_additional_options( &mut self, ) -> Result { + let opt_ilike = if dialect_of!(self is GenericDialect | SnowflakeDialect) { + self.parse_optional_select_item_ilike()? + } else { + None + }; let opt_exclude = if dialect_of!(self is GenericDialect | DuckDbDialect | SnowflakeDialect) { - self.parse_optional_select_item_exclude()? + self.parse_optional_select_item_exclude(opt_ilike.is_some())? } else { None }; @@ -8801,6 +8806,7 @@ impl<'a> Parser<'a> { }; Ok(WildcardAdditionalOptions { + opt_ilike, opt_exclude, opt_except, opt_rename, @@ -8808,13 +8814,31 @@ impl<'a> Parser<'a> { }) } + pub fn parse_optional_select_item_ilike( + &mut self, + ) -> Result, ParserError> { + let opt_ilike = if self.parse_keyword(Keyword::ILIKE) { + let pattern = self.parse_value()?; + Some(IlikeSelectItem { + pattern: Expr::Value(pattern), + }) + } else { + None + }; + Ok(opt_ilike) + } + /// Parse an [`Exclude`](ExcludeSelectItem) information for wildcard select items. /// /// If it is not possible to parse it, will return an option. pub fn parse_optional_select_item_exclude( &mut self, + opt_ilike: bool, ) -> Result, ParserError> { let opt_exclude = if self.parse_keyword(Keyword::EXCLUDE) { + if opt_ilike { + return Err(ParserError::ParserError("Unexpected EXCLUDE".to_string())); + } if self.consume_token(&Token::LParen) { let columns = self.parse_comma_separated(|parser| parser.parse_identifier(false))?; diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index b31fa379a..985de2820 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -6560,6 +6560,7 @@ fn lateral_function() { distinct: None, top: None, projection: vec![SelectItem::Wildcard(WildcardAdditionalOptions { + opt_ilike: None, opt_exclude: None, opt_except: None, opt_rename: None, diff --git a/tests/sqlparser_duckdb.rs b/tests/sqlparser_duckdb.rs index 469608488..d36dc0db9 100644 --- a/tests/sqlparser_duckdb.rs +++ b/tests/sqlparser_duckdb.rs @@ -148,6 +148,7 @@ fn test_select_union_by_name() { distinct: None, top: None, projection: vec![SelectItem::Wildcard(WildcardAdditionalOptions { + opt_ilike: None, opt_exclude: None, opt_except: None, opt_rename: None, @@ -183,6 +184,7 @@ fn test_select_union_by_name() { distinct: None, top: None, projection: vec![SelectItem::Wildcard(WildcardAdditionalOptions { + opt_ilike: None, opt_exclude: None, opt_except: None, opt_rename: None, diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index 95b0d4330..135781fc4 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -1615,3 +1615,24 @@ fn test_select_wildcard_with_replace() { }); assert_eq!(expected, select.projection[0]); } + +#[test] +fn test_select_wildcard_with_ilike() { + let select = snowflake_and_generic().verified_only_select(r#"SELECT * ILIKE '%id%' FROM tbl"#); + let expected = SelectItem::Wildcard(WildcardAdditionalOptions { + opt_ilike: Some(IlikeSelectItem { + pattern: Expr::Value(Value::SingleQuotedString("%id%".to_owned())), + }), + ..Default::default() + }); + assert_eq!(expected, select.projection[0]); +} + +#[test] +fn test_select_wildcard_with_ilike_replace() { + let res = snowflake().parse_sql_statements(r#"SELECT * ILIKE '%id%' EXCLUDE col FROM tbl"#); + assert_eq!( + res.unwrap_err().to_string(), + "sql parser error: Unexpected EXCLUDE" + ); +} From 053b2516e9910a687e28809552ed60287fc35671 Mon Sep 17 00:00:00 2001 From: Hiranmaya Gundu Date: Fri, 19 Apr 2024 17:25:44 -0700 Subject: [PATCH 2/7] fix: pr comments --- src/ast/query.rs | 4 ++-- src/parser/mod.rs | 6 ++---- tests/sqlparser_snowflake.rs | 20 +++++++++++++++++++- 3 files changed, 23 insertions(+), 7 deletions(-) diff --git a/src/ast/query.rs b/src/ast/query.rs index 34ad74da6..ad5727cef 100644 --- a/src/ast/query.rs +++ b/src/ast/query.rs @@ -524,12 +524,12 @@ impl fmt::Display for WildcardAdditionalOptions { #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] pub struct IlikeSelectItem { - pub pattern: Expr, + pub pattern: String, } impl fmt::Display for IlikeSelectItem { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "ILIKE {}", self.pattern)?; + write!(f, "ILIKE '{}'", self.pattern)?; Ok(()) } } diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 0515b1b60..26845978b 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -8818,10 +8818,8 @@ impl<'a> Parser<'a> { &mut self, ) -> Result, ParserError> { let opt_ilike = if self.parse_keyword(Keyword::ILIKE) { - let pattern = self.parse_value()?; - Some(IlikeSelectItem { - pattern: Expr::Value(pattern), - }) + let pattern = self.parse_literal_string()?; + Some(IlikeSelectItem { pattern }) } else { None }; diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index 135781fc4..ca8958fd4 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -1621,13 +1621,31 @@ fn test_select_wildcard_with_ilike() { let select = snowflake_and_generic().verified_only_select(r#"SELECT * ILIKE '%id%' FROM tbl"#); let expected = SelectItem::Wildcard(WildcardAdditionalOptions { opt_ilike: Some(IlikeSelectItem { - pattern: Expr::Value(Value::SingleQuotedString("%id%".to_owned())), + pattern: "%id%".to_owned(), }), ..Default::default() }); assert_eq!(expected, select.projection[0]); } +#[test] +fn test_select_wildcard_with_ilike_non_literal() { + let res = snowflake().parse_sql_statements(r#"SELECT * ILIKE %id FROM tbl"#); + assert_eq!( + res.unwrap_err().to_string(), + "sql parser error: Expected literal string, found: %" + ); +} + +#[test] +fn test_select_wildcard_with_ilike_number() { + let res = snowflake().parse_sql_statements(r#"SELECT * ILIKE 42 FROM tbl"#); + assert_eq!( + res.unwrap_err().to_string(), + "sql parser error: Expected literal string, found: 42" + ); +} + #[test] fn test_select_wildcard_with_ilike_replace() { let res = snowflake().parse_sql_statements(r#"SELECT * ILIKE '%id%' EXCLUDE col FROM tbl"#); From 1823d05f83790feaa4242bbf2d20b0b4214530d2 Mon Sep 17 00:00:00 2001 From: Hiranmaya Gundu Date: Fri, 19 Apr 2024 17:45:15 -0700 Subject: [PATCH 3/7] fix: pr comments --- src/ast/query.rs | 6 +++++- src/parser/mod.rs | 17 +++++++++-------- tests/sqlparser_snowflake.rs | 8 ++++---- 3 files changed, 18 insertions(+), 13 deletions(-) diff --git a/src/ast/query.rs b/src/ast/query.rs index ad5727cef..a2bfea102 100644 --- a/src/ast/query.rs +++ b/src/ast/query.rs @@ -529,7 +529,11 @@ pub struct IlikeSelectItem { impl fmt::Display for IlikeSelectItem { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "ILIKE '{}'", self.pattern)?; + write!( + f, + "ILIKE '{}'", + value::escape_single_quote_string(&self.pattern) + )?; Ok(()) } } diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 26845978b..a2244ddc6 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -8780,9 +8780,10 @@ impl<'a> Parser<'a> { } else { None }; - let opt_exclude = if dialect_of!(self is GenericDialect | DuckDbDialect | SnowflakeDialect) + let opt_exclude = if !opt_ilike.is_some() + && dialect_of!(self is GenericDialect | DuckDbDialect | SnowflakeDialect) { - self.parse_optional_select_item_exclude(opt_ilike.is_some())? + self.parse_optional_select_item_exclude()? } else { None }; @@ -8818,8 +8819,12 @@ impl<'a> Parser<'a> { &mut self, ) -> Result, ParserError> { let opt_ilike = if self.parse_keyword(Keyword::ILIKE) { - let pattern = self.parse_literal_string()?; - Some(IlikeSelectItem { pattern }) + let next_token = self.next_token(); + let pattern = match next_token.token { + Token::SingleQuotedString(s) => Ok(s), + _ => self.expected("single quoted string", next_token), + }; + Some(IlikeSelectItem { pattern: pattern? }) } else { None }; @@ -8831,12 +8836,8 @@ impl<'a> Parser<'a> { /// If it is not possible to parse it, will return an option. pub fn parse_optional_select_item_exclude( &mut self, - opt_ilike: bool, ) -> Result, ParserError> { let opt_exclude = if self.parse_keyword(Keyword::EXCLUDE) { - if opt_ilike { - return Err(ParserError::ParserError("Unexpected EXCLUDE".to_string())); - } if self.consume_token(&Token::LParen) { let columns = self.parse_comma_separated(|parser| parser.parse_identifier(false))?; diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index ca8958fd4..c3d41f2a1 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -1629,11 +1629,11 @@ fn test_select_wildcard_with_ilike() { } #[test] -fn test_select_wildcard_with_ilike_non_literal() { - let res = snowflake().parse_sql_statements(r#"SELECT * ILIKE %id FROM tbl"#); +fn test_select_wildcard_with_ilike_double_quote() { + let res = snowflake().parse_sql_statements(r#"SELECT * ILIKE "%id" FROM tbl"#); assert_eq!( res.unwrap_err().to_string(), - "sql parser error: Expected literal string, found: %" + "sql parser error: Expected single quoted string, found: \"%id\"" ); } @@ -1651,6 +1651,6 @@ fn test_select_wildcard_with_ilike_replace() { let res = snowflake().parse_sql_statements(r#"SELECT * ILIKE '%id%' EXCLUDE col FROM tbl"#); assert_eq!( res.unwrap_err().to_string(), - "sql parser error: Unexpected EXCLUDE" + "sql parser error: Expected end of statement, found: EXCLUDE" ); } From 5449d01f7653d8ca15f3ea1fd0b976c9b1baf17b Mon Sep 17 00:00:00 2001 From: Hiranmaya Gundu Date: Fri, 19 Apr 2024 17:50:50 -0700 Subject: [PATCH 4/7] fix: pr comments --- src/parser/mod.rs | 2 +- tests/sqlparser_snowflake.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index a2244ddc6..8c68a13aa 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -8780,7 +8780,7 @@ impl<'a> Parser<'a> { } else { None }; - let opt_exclude = if !opt_ilike.is_some() + let opt_exclude = if opt_ilike.is_none() && dialect_of!(self is GenericDialect | DuckDbDialect | SnowflakeDialect) { self.parse_optional_select_item_exclude()? diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index c3d41f2a1..326b36259 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -1642,7 +1642,7 @@ fn test_select_wildcard_with_ilike_number() { let res = snowflake().parse_sql_statements(r#"SELECT * ILIKE 42 FROM tbl"#); assert_eq!( res.unwrap_err().to_string(), - "sql parser error: Expected literal string, found: 42" + "sql parser error: Expected single quoted string, found: 42" ); } From 8be8f27cedb3d019a80378f9933c10170984e1ad Mon Sep 17 00:00:00 2001 From: Hiranmaya Gundu Date: Fri, 19 Apr 2024 18:02:40 -0700 Subject: [PATCH 5/7] fix: change to ilike pattern --- src/parser/mod.rs | 2 +- tests/sqlparser_snowflake.rs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 8c68a13aa..61ac5c02a 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -8822,7 +8822,7 @@ impl<'a> Parser<'a> { let next_token = self.next_token(); let pattern = match next_token.token { Token::SingleQuotedString(s) => Ok(s), - _ => self.expected("single quoted string", next_token), + _ => self.expected("ilike pattern", next_token), }; Some(IlikeSelectItem { pattern: pattern? }) } else { diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index 326b36259..9442e330a 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -1633,7 +1633,7 @@ fn test_select_wildcard_with_ilike_double_quote() { let res = snowflake().parse_sql_statements(r#"SELECT * ILIKE "%id" FROM tbl"#); assert_eq!( res.unwrap_err().to_string(), - "sql parser error: Expected single quoted string, found: \"%id\"" + "sql parser error: Expected ilike pattern, found: \"%id\"" ); } @@ -1642,7 +1642,7 @@ fn test_select_wildcard_with_ilike_number() { let res = snowflake().parse_sql_statements(r#"SELECT * ILIKE 42 FROM tbl"#); assert_eq!( res.unwrap_err().to_string(), - "sql parser error: Expected single quoted string, found: 42" + "sql parser error: Expected ilike pattern, found: 42" ); } From eb3e22199efee6010183b40c174e1e0d1a6799a3 Mon Sep 17 00:00:00 2001 From: Hiranmaya Gundu Date: Fri, 19 Apr 2024 18:04:50 -0700 Subject: [PATCH 6/7] fix: return early --- src/parser/mod.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 61ac5c02a..20085ba7a 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -8821,10 +8821,10 @@ impl<'a> Parser<'a> { let opt_ilike = if self.parse_keyword(Keyword::ILIKE) { let next_token = self.next_token(); let pattern = match next_token.token { - Token::SingleQuotedString(s) => Ok(s), - _ => self.expected("ilike pattern", next_token), + Token::SingleQuotedString(s) => s, + _ => return self.expected("ilike pattern", next_token), }; - Some(IlikeSelectItem { pattern: pattern? }) + Some(IlikeSelectItem { pattern: pattern }) } else { None }; From 8ef923f32201cfeb48113d3257b2bea745de8fc9 Mon Sep 17 00:00:00 2001 From: Hiranmaya Gundu Date: Fri, 19 Apr 2024 18:04:58 -0700 Subject: [PATCH 7/7] fix: return early --- src/parser/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 20085ba7a..816c175b7 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -8824,7 +8824,7 @@ impl<'a> Parser<'a> { Token::SingleQuotedString(s) => s, _ => return self.expected("ilike pattern", next_token), }; - Some(IlikeSelectItem { pattern: pattern }) + Some(IlikeSelectItem { pattern }) } else { None };