Skip to content

Commit 9889b24

Browse files
feat: implement wildcard select ilike (apache#22)
Implements parsing for `SELECT * ILIKE ...` which is valid syntax in snowflake: https://docs.snowflake.com/en/sql-reference/sql/select Via googling, it seems like only snowflake supports this syntax. I could not find any other dialect that does. Snowflake does not allow `SELECT * ILIKE <pattern> EXCLUDE <column>` so I implemented that as well.
2 parents 16cdc92 + 8ef923f commit 9889b24

File tree

6 files changed

+97
-3
lines changed

6 files changed

+97
-3
lines changed

src/ast/mod.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,8 @@ pub use self::ddl::{
4040
pub use self::operator::{BinaryOperator, UnaryOperator};
4141
pub use self::query::{
4242
ConnectBy, Cte, CteAsMaterialized, Distinct, ExceptSelectItem, ExcludeSelectItem, Fetch,
43-
ForClause, ForJson, ForXml, GroupByExpr, IdentWithAlias, Join, JoinConstraint, JoinOperator,
44-
JsonTableColumn, JsonTableColumnErrorHandling, LateralView, LockClause, LockType,
43+
ForClause, ForJson, ForXml, GroupByExpr, IdentWithAlias, IlikeSelectItem, Join, JoinConstraint,
44+
JoinOperator, JsonTableColumn, JsonTableColumnErrorHandling, LateralView, LockClause, LockType,
4545
NamedWindowDefinition, NonBlock, Offset, OffsetRows, OrderByExpr, Query, RenameSelectItem,
4646
ReplaceSelectElement, ReplaceSelectItem, Select, SelectInto, SelectItem, SetExpr, SetOperator,
4747
SetQuantifier, Table, TableAlias, TableFactor, TableVersion, TableWithJoins, Top, TopQuantity,

src/ast/query.rs

+29
Original file line numberDiff line numberDiff line change
@@ -477,6 +477,9 @@ impl fmt::Display for IdentWithAlias {
477477
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
478478
#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
479479
pub struct WildcardAdditionalOptions {
480+
/// `[ILIKE...]`.
481+
/// Snowflake syntax: <https://docs.snowflake.com/en/sql-reference/sql/select>
482+
pub opt_ilike: Option<IlikeSelectItem>,
480483
/// `[EXCLUDE...]`.
481484
pub opt_exclude: Option<ExcludeSelectItem>,
482485
/// `[EXCEPT...]`.
@@ -492,6 +495,9 @@ pub struct WildcardAdditionalOptions {
492495

493496
impl fmt::Display for WildcardAdditionalOptions {
494497
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
498+
if let Some(ilike) = &self.opt_ilike {
499+
write!(f, " {ilike}")?;
500+
}
495501
if let Some(exclude) = &self.opt_exclude {
496502
write!(f, " {exclude}")?;
497503
}
@@ -508,6 +514,29 @@ impl fmt::Display for WildcardAdditionalOptions {
508514
}
509515
}
510516

517+
/// Snowflake `ILIKE` information.
518+
///
519+
/// # Syntax
520+
/// ```plaintext
521+
/// ILIKE <value>
522+
/// ```
523+
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
524+
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
525+
#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
526+
pub struct IlikeSelectItem {
527+
pub pattern: String,
528+
}
529+
530+
impl fmt::Display for IlikeSelectItem {
531+
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
532+
write!(
533+
f,
534+
"ILIKE '{}'",
535+
value::escape_single_quote_string(&self.pattern)
536+
)?;
537+
Ok(())
538+
}
539+
}
511540
/// Snowflake `EXCLUDE` information.
512541
///
513542
/// # Syntax

src/parser/mod.rs

+24-1
Original file line numberDiff line numberDiff line change
@@ -8775,7 +8775,13 @@ impl<'a> Parser<'a> {
87758775
pub fn parse_wildcard_additional_options(
87768776
&mut self,
87778777
) -> Result<WildcardAdditionalOptions, ParserError> {
8778-
let opt_exclude = if dialect_of!(self is GenericDialect | DuckDbDialect | SnowflakeDialect)
8778+
let opt_ilike = if dialect_of!(self is GenericDialect | SnowflakeDialect) {
8779+
self.parse_optional_select_item_ilike()?
8780+
} else {
8781+
None
8782+
};
8783+
let opt_exclude = if opt_ilike.is_none()
8784+
&& dialect_of!(self is GenericDialect | DuckDbDialect | SnowflakeDialect)
87798785
{
87808786
self.parse_optional_select_item_exclude()?
87818787
} else {
@@ -8801,13 +8807,30 @@ impl<'a> Parser<'a> {
88018807
};
88028808

88038809
Ok(WildcardAdditionalOptions {
8810+
opt_ilike,
88048811
opt_exclude,
88058812
opt_except,
88068813
opt_rename,
88078814
opt_replace,
88088815
})
88098816
}
88108817

8818+
pub fn parse_optional_select_item_ilike(
8819+
&mut self,
8820+
) -> Result<Option<IlikeSelectItem>, ParserError> {
8821+
let opt_ilike = if self.parse_keyword(Keyword::ILIKE) {
8822+
let next_token = self.next_token();
8823+
let pattern = match next_token.token {
8824+
Token::SingleQuotedString(s) => s,
8825+
_ => return self.expected("ilike pattern", next_token),
8826+
};
8827+
Some(IlikeSelectItem { pattern })
8828+
} else {
8829+
None
8830+
};
8831+
Ok(opt_ilike)
8832+
}
8833+
88118834
/// Parse an [`Exclude`](ExcludeSelectItem) information for wildcard select items.
88128835
///
88138836
/// If it is not possible to parse it, will return an option.

tests/sqlparser_common.rs

+1
Original file line numberDiff line numberDiff line change
@@ -6560,6 +6560,7 @@ fn lateral_function() {
65606560
distinct: None,
65616561
top: None,
65626562
projection: vec![SelectItem::Wildcard(WildcardAdditionalOptions {
6563+
opt_ilike: None,
65636564
opt_exclude: None,
65646565
opt_except: None,
65656566
opt_rename: None,

tests/sqlparser_duckdb.rs

+2
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,7 @@ fn test_select_union_by_name() {
148148
distinct: None,
149149
top: None,
150150
projection: vec![SelectItem::Wildcard(WildcardAdditionalOptions {
151+
opt_ilike: None,
151152
opt_exclude: None,
152153
opt_except: None,
153154
opt_rename: None,
@@ -183,6 +184,7 @@ fn test_select_union_by_name() {
183184
distinct: None,
184185
top: None,
185186
projection: vec![SelectItem::Wildcard(WildcardAdditionalOptions {
187+
opt_ilike: None,
186188
opt_exclude: None,
187189
opt_except: None,
188190
opt_rename: None,

tests/sqlparser_snowflake.rs

+39
Original file line numberDiff line numberDiff line change
@@ -1615,3 +1615,42 @@ fn test_select_wildcard_with_replace() {
16151615
});
16161616
assert_eq!(expected, select.projection[0]);
16171617
}
1618+
1619+
#[test]
1620+
fn test_select_wildcard_with_ilike() {
1621+
let select = snowflake_and_generic().verified_only_select(r#"SELECT * ILIKE '%id%' FROM tbl"#);
1622+
let expected = SelectItem::Wildcard(WildcardAdditionalOptions {
1623+
opt_ilike: Some(IlikeSelectItem {
1624+
pattern: "%id%".to_owned(),
1625+
}),
1626+
..Default::default()
1627+
});
1628+
assert_eq!(expected, select.projection[0]);
1629+
}
1630+
1631+
#[test]
1632+
fn test_select_wildcard_with_ilike_double_quote() {
1633+
let res = snowflake().parse_sql_statements(r#"SELECT * ILIKE "%id" FROM tbl"#);
1634+
assert_eq!(
1635+
res.unwrap_err().to_string(),
1636+
"sql parser error: Expected ilike pattern, found: \"%id\""
1637+
);
1638+
}
1639+
1640+
#[test]
1641+
fn test_select_wildcard_with_ilike_number() {
1642+
let res = snowflake().parse_sql_statements(r#"SELECT * ILIKE 42 FROM tbl"#);
1643+
assert_eq!(
1644+
res.unwrap_err().to_string(),
1645+
"sql parser error: Expected ilike pattern, found: 42"
1646+
);
1647+
}
1648+
1649+
#[test]
1650+
fn test_select_wildcard_with_ilike_replace() {
1651+
let res = snowflake().parse_sql_statements(r#"SELECT * ILIKE '%id%' EXCLUDE col FROM tbl"#);
1652+
assert_eq!(
1653+
res.unwrap_err().to_string(),
1654+
"sql parser error: Expected end of statement, found: EXCLUDE"
1655+
);
1656+
}

0 commit comments

Comments
 (0)