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..a2bfea102 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,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 8ddff71fe..816c175b7 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -8775,7 +8775,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 { @@ -8801,6 +8807,7 @@ impl<'a> Parser<'a> { }; Ok(WildcardAdditionalOptions { + opt_ilike, opt_exclude, opt_except, opt_rename, @@ -8808,6 +8815,22 @@ 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 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 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..9442e330a 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -1615,3 +1615,42 @@ 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: "%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" + ); +}