From 76bfa87ee2487d16738132c11bfec85ffcfcff01 Mon Sep 17 00:00:00 2001 From: Hiranmaya Gundu Date: Sun, 21 Apr 2024 05:22:08 -0700 Subject: [PATCH] feat: implement select * ilike for snowflake (#1228) --- src/ast/mod.rs | 4 ++-- src/ast/query.rs | 29 +++++++++++++++++++++++++++ src/parser/mod.rs | 28 +++++++++++++++++++++++++- tests/sqlparser_common.rs | 1 + tests/sqlparser_duckdb.rs | 2 ++ tests/sqlparser_snowflake.rs | 39 ++++++++++++++++++++++++++++++++++++ 6 files changed, 100 insertions(+), 3 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index a2a9da2ba..16ead77e8 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::{ Cte, CteAsMaterialized, Distinct, ExceptSelectItem, ExcludeSelectItem, Fetch, ForClause, - ForJson, ForXml, GroupByExpr, IdentWithAlias, Join, JoinConstraint, JoinOperator, - JsonTableColumn, JsonTableColumnErrorHandling, LateralView, LockClause, LockType, + 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 bf33cdee6..391ef51d8 100644 --- a/src/ast/query.rs +++ b/src/ast/query.rs @@ -474,6 +474,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...]`. @@ -489,6 +492,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}")?; } @@ -505,6 +511,29 @@ 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: String, +} + +impl fmt::Display for IlikeSelectItem { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "ILIKE '{}'", + value::escape_single_quote_string(&self.pattern) + )?; + Ok(()) + } +} /// Snowflake `EXCLUDE` information. /// /// # Syntax diff --git a/src/parser/mod.rs b/src/parser/mod.rs index daae8199b..21bc9e36c 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -9018,7 +9018,13 @@ impl<'a> Parser<'a> { pub fn parse_wildcard_additional_options( &mut self, ) -> Result { - let opt_exclude = if dialect_of!(self is GenericDialect | DuckDbDialect | SnowflakeDialect) + let opt_ilike = if dialect_of!(self is GenericDialect | SnowflakeDialect) { + self.parse_optional_select_item_ilike()? + } else { + None + }; + let opt_exclude = if opt_ilike.is_none() + && dialect_of!(self is GenericDialect | DuckDbDialect | SnowflakeDialect) { self.parse_optional_select_item_exclude()? } else { @@ -9044,6 +9050,7 @@ impl<'a> Parser<'a> { }; Ok(WildcardAdditionalOptions { + opt_ilike, opt_exclude, opt_except, opt_rename, @@ -9051,6 +9058,25 @@ impl<'a> Parser<'a> { }) } + /// Parse an [`Ilike`](IlikeSelectItem) information for wildcard select items. + /// + /// If it is not possible to parse it, will return an option. + pub fn parse_optional_select_item_ilike( + &mut self, + ) -> Result, ParserError> { + let opt_ilike = if self.parse_keyword(Keyword::ILIKE) { + let next_token = self.next_token(); + let pattern = match next_token.token { + Token::SingleQuotedString(s) => s, + _ => return self.expected("ilike pattern", next_token), + }; + Some(IlikeSelectItem { 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. diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 3aa84b923..bbc0f0b2f 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -6622,6 +6622,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 5dd7280de..02c8c7dc5 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 b76e84ed4..56060a0d7 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -1555,3 +1555,42 @@ fn parse_comma_outer_join() { fn test_sf_trailing_commas() { snowflake().verified_only_select_with_canonical("SELECT 1, 2, FROM t", "SELECT 1, 2 FROM t"); } + +#[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: "%id%".to_owned(), + }), + ..Default::default() + }); + assert_eq!(expected, select.projection[0]); +} + +#[test] +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 ilike pattern, found: \"%id\"" + ); +} + +#[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 ilike pattern, found: 42" + ); +} + +#[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: Expected end of statement, found: EXCLUDE" + ); +}