Skip to content

Commit e393b20

Browse files
committed
Add support for Snowflake column aliases that use SQL keywords
1 parent fe36020 commit e393b20

File tree

4 files changed

+111
-1
lines changed

4 files changed

+111
-1
lines changed

src/dialect/mod.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -768,6 +768,13 @@ pub trait Dialect: Debug + Any {
768768
fn supports_table_sample_before_alias(&self) -> bool {
769769
false
770770
}
771+
772+
/// Returns true if the specified keyword should be parsed as a select item alias.
773+
/// When explicit is true, the keyword is preceded by an `AS` word. Parser is provided
774+
/// to enable looking ahead if needed.
775+
fn is_select_item_alias(&self, explicit: bool, kw: &Keyword, _parser: &mut Parser) -> bool {
776+
explicit || !keywords::RESERVED_FOR_COLUMN_ALIAS.contains(kw)
777+
}
771778
}
772779

773780
/// This represents the operators for which precedence must be defined

src/dialect/snowflake.rs

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,50 @@ impl Dialect for SnowflakeDialect {
238238
fn supports_partiql(&self) -> bool {
239239
true
240240
}
241+
242+
fn is_select_item_alias(&self, explicit: bool, kw: &Keyword, parser: &mut Parser) -> bool {
243+
explicit
244+
|| match kw {
245+
// The following keywords can be considered an alias as long as
246+
// they are not followed by other tokens that may change their meaning
247+
// e.g. `SELECT * EXCEPT (col1) FROM tbl`
248+
Keyword::EXCEPT
249+
// e.g. `SELECT 1 LIMIT 5`
250+
| Keyword::LIMIT
251+
// e.g. `SELECT 1 OFFSET 5 ROWS`
252+
| Keyword::OFFSET
253+
// e.g. `INSERT INTO t SELECT 1 RETURNING *`
254+
| Keyword::RETURNING if !matches!(parser.peek_token_ref().token, Token::Comma | Token::EOF) =>
255+
{
256+
false
257+
}
258+
259+
// `FETCH` can be considered an alias as long as it's not followed by `FIRST`` or `NEXT`
260+
// which would give it a different meanins, for example: `SELECT 1 FETCH FIRST 10 ROWS` - not an alias
261+
Keyword::FETCH
262+
if parser.peek_keyword(Keyword::FIRST) || parser.peek_keyword(Keyword::NEXT) =>
263+
{
264+
false
265+
}
266+
267+
// Reserved keywords by the Snowflake dialect, which seem to be less strictive
268+
// than what is listed in `keywords::RESERVED_FOR_COLUMN_ALIAS`. The following
269+
// keywords were tested with the this statement: `SELECT 1 <KW>`.
270+
Keyword::FROM
271+
| Keyword::GROUP
272+
| Keyword::HAVING
273+
| Keyword::INTERSECT
274+
| Keyword::INTO
275+
| Keyword::ORDER
276+
| Keyword::SELECT
277+
| Keyword::UNION
278+
| Keyword::WHERE
279+
| Keyword::WITH => false,
280+
281+
// Any other word is considered an alias
282+
_ => true,
283+
}
284+
}
241285
}
242286

243287
/// Parse snowflake create table statement.

src/parser/mod.rs

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8719,6 +8719,34 @@ impl<'a> Parser<'a> {
87198719
Ok(IdentWithAlias { ident, alias })
87208720
}
87218721

8722+
// Optionally parses an alias for a select list item
8723+
fn maybe_parse_select_item_alias(&mut self) -> Result<Option<Ident>, ParserError> {
8724+
let after_as = self.parse_keyword(Keyword::AS);
8725+
let next_token = self.next_token();
8726+
match next_token.token {
8727+
// Dialect-specific behavior for words that may be reserved from parsed
8728+
// as select item aliases.
8729+
Token::Word(w)
8730+
if self
8731+
.dialect
8732+
.is_select_item_alias(after_as, &w.keyword, self) =>
8733+
{
8734+
Ok(Some(w.into_ident(next_token.span)))
8735+
}
8736+
// MSSQL supports single-quoted strings as aliases for columns
8737+
Token::SingleQuotedString(s) => Ok(Some(Ident::with_quote('\'', s))),
8738+
// Support for MySql dialect double-quoted string, `AS "HOUR"` for example
8739+
Token::DoubleQuotedString(s) => Ok(Some(Ident::with_quote('\"', s))),
8740+
_ => {
8741+
if after_as {
8742+
return self.expected("an identifier after AS", next_token);
8743+
}
8744+
self.prev_token();
8745+
Ok(None) // no alias found
8746+
}
8747+
}
8748+
}
8749+
87228750
/// Parse `AS identifier` (or simply `identifier` if it's not a reserved keyword)
87238751
/// Some examples with aliases: `SELECT 1 foo`, `SELECT COUNT(*) AS cnt`,
87248752
/// `SELECT ... FROM t1 foo, t2 bar`, `SELECT ... FROM (...) AS bar`
@@ -8737,6 +8765,8 @@ impl<'a> Parser<'a> {
87378765
Token::Word(w) if after_as || !reserved_kwds.contains(&w.keyword) => {
87388766
Ok(Some(w.into_ident(next_token.span)))
87398767
}
8768+
// Left the next two patterns for backwards-compatibility (despite not breaking
8769+
// any tests).
87408770
// MSSQL supports single-quoted strings as aliases for columns
87418771
// We accept them as table aliases too, although MSSQL does not.
87428772
//
@@ -12280,7 +12310,7 @@ impl<'a> Parser<'a> {
1228012310
})
1228112311
}
1228212312
expr => self
12283-
.parse_optional_alias(keywords::RESERVED_FOR_COLUMN_ALIAS)
12313+
.maybe_parse_select_item_alias()
1228412314
.map(|alias| match alias {
1228512315
Some(alias) => SelectItem::ExprWithAlias { expr, alias },
1228612316
None => SelectItem::UnnamedExpr(expr),

tests/sqlparser_snowflake.rs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2983,3 +2983,32 @@ fn test_table_sample() {
29832983
snowflake_and_generic().verified_stmt("SELECT id FROM mytable TABLESAMPLE (10) REPEATABLE (1)");
29842984
snowflake_and_generic().verified_stmt("SELECT id FROM mytable TABLESAMPLE (10) SEED (1)");
29852985
}
2986+
2987+
#[test]
2988+
fn test_sql_keywords_as_select_item_aliases() {
2989+
// Some keywords that should be parsed as an alias
2990+
let unreserved_kws = vec!["CLUSTER", "FETCH", "RETURNING", "LIMIT", "EXCEPT"];
2991+
for kw in unreserved_kws {
2992+
snowflake()
2993+
.one_statement_parses_to(&format!("SELECT 1 {kw}"), &format!("SELECT 1 AS {kw}"));
2994+
}
2995+
2996+
// Some keywords that should not be parsed as an alias
2997+
let reserved_kws = vec![
2998+
"FROM",
2999+
"GROUP",
3000+
"HAVING",
3001+
"INTERSECT",
3002+
"INTO",
3003+
"ORDER",
3004+
"SELECT",
3005+
"UNION",
3006+
"WHERE",
3007+
"WITH",
3008+
];
3009+
for kw in reserved_kws {
3010+
assert!(snowflake()
3011+
.parse_sql_statements(&format!("SELECT 1 {kw}"))
3012+
.is_err());
3013+
}
3014+
}

0 commit comments

Comments
 (0)