Skip to content

Commit 74a9ac9

Browse files
committed
Add support for Snowflake column aliases that use SQL keywords
1 parent 36db176 commit 74a9ac9

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
@@ -820,6 +820,13 @@ pub trait Dialect: Debug + Any {
820820
fn supports_set_stmt_without_operator(&self) -> bool {
821821
false
822822
}
823+
824+
/// Returns true if the specified keyword should be parsed as a select item alias.
825+
/// When explicit is true, the keyword is preceded by an `AS` word. Parser is provided
826+
/// to enable looking ahead if needed.
827+
fn is_select_item_alias(&self, explicit: bool, kw: &Keyword, _parser: &mut Parser) -> bool {
828+
explicit || !keywords::RESERVED_FOR_COLUMN_ALIAS.contains(kw)
829+
}
823830
}
824831

825832
/// 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
@@ -251,6 +251,50 @@ impl Dialect for SnowflakeDialect {
251251
fn supports_partiql(&self) -> bool {
252252
true
253253
}
254+
255+
fn is_select_item_alias(&self, explicit: bool, kw: &Keyword, parser: &mut Parser) -> bool {
256+
explicit
257+
|| match kw {
258+
// The following keywords can be considered an alias as long as
259+
// they are not followed by other tokens that may change their meaning
260+
// e.g. `SELECT * EXCEPT (col1) FROM tbl`
261+
Keyword::EXCEPT
262+
// e.g. `SELECT 1 LIMIT 5`
263+
| Keyword::LIMIT
264+
// e.g. `SELECT 1 OFFSET 5 ROWS`
265+
| Keyword::OFFSET
266+
// e.g. `INSERT INTO t SELECT 1 RETURNING *`
267+
| Keyword::RETURNING if !matches!(parser.peek_token_ref().token, Token::Comma | Token::EOF) =>
268+
{
269+
false
270+
}
271+
272+
// `FETCH` can be considered an alias as long as it's not followed by `FIRST`` or `NEXT`
273+
// which would give it a different meanins, for example: `SELECT 1 FETCH FIRST 10 ROWS` - not an alias
274+
Keyword::FETCH
275+
if parser.peek_keyword(Keyword::FIRST) || parser.peek_keyword(Keyword::NEXT) =>
276+
{
277+
false
278+
}
279+
280+
// Reserved keywords by the Snowflake dialect, which seem to be less strictive
281+
// than what is listed in `keywords::RESERVED_FOR_COLUMN_ALIAS`. The following
282+
// keywords were tested with the this statement: `SELECT 1 <KW>`.
283+
Keyword::FROM
284+
| Keyword::GROUP
285+
| Keyword::HAVING
286+
| Keyword::INTERSECT
287+
| Keyword::INTO
288+
| Keyword::ORDER
289+
| Keyword::SELECT
290+
| Keyword::UNION
291+
| Keyword::WHERE
292+
| Keyword::WITH => false,
293+
294+
// Any other word is considered an alias
295+
_ => true,
296+
}
297+
}
254298
}
255299

256300
fn parse_file_staging_command(kw: Keyword, parser: &mut Parser) -> Result<Statement, ParserError> {

src/parser/mod.rs

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

8840+
// Optionally parses an alias for a select list item
8841+
fn maybe_parse_select_item_alias(&mut self) -> Result<Option<Ident>, ParserError> {
8842+
let after_as = self.parse_keyword(Keyword::AS);
8843+
let next_token = self.next_token();
8844+
match next_token.token {
8845+
// Dialect-specific behavior for words that may be reserved from parsed
8846+
// as select item aliases.
8847+
Token::Word(w)
8848+
if self
8849+
.dialect
8850+
.is_select_item_alias(after_as, &w.keyword, self) =>
8851+
{
8852+
Ok(Some(w.into_ident(next_token.span)))
8853+
}
8854+
// MSSQL supports single-quoted strings as aliases for columns
8855+
Token::SingleQuotedString(s) => Ok(Some(Ident::with_quote('\'', s))),
8856+
// Support for MySql dialect double-quoted string, `AS "HOUR"` for example
8857+
Token::DoubleQuotedString(s) => Ok(Some(Ident::with_quote('\"', s))),
8858+
_ => {
8859+
if after_as {
8860+
return self.expected("an identifier after AS", next_token);
8861+
}
8862+
self.prev_token();
8863+
Ok(None) // no alias found
8864+
}
8865+
}
8866+
}
8867+
88408868
/// Parse `AS identifier` (or simply `identifier` if it's not a reserved keyword)
88418869
/// Some examples with aliases: `SELECT 1 foo`, `SELECT COUNT(*) AS cnt`,
88428870
/// `SELECT ... FROM t1 foo, t2 bar`, `SELECT ... FROM (...) AS bar`
@@ -8855,6 +8883,8 @@ impl<'a> Parser<'a> {
88558883
Token::Word(w) if after_as || !reserved_kwds.contains(&w.keyword) => {
88568884
Ok(Some(w.into_ident(next_token.span)))
88578885
}
8886+
// Left the next two patterns for backwards-compatibility (despite not breaking
8887+
// any tests).
88588888
// MSSQL supports single-quoted strings as aliases for columns
88598889
// We accept them as table aliases too, although MSSQL does not.
88608890
//
@@ -12613,7 +12643,7 @@ impl<'a> Parser<'a> {
1261312643
})
1261412644
}
1261512645
expr => self
12616-
.parse_optional_alias(keywords::RESERVED_FOR_COLUMN_ALIAS)
12646+
.maybe_parse_select_item_alias()
1261712647
.map(|alias| match alias {
1261812648
Some(alias) => SelectItem::ExprWithAlias { expr, alias },
1261912649
None => SelectItem::UnnamedExpr(expr),

tests/sqlparser_snowflake.rs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3022,3 +3022,32 @@ fn parse_ls_and_rm() {
30223022

30233023
snowflake().verified_stmt(r#"LIST @"STAGE_WITH_QUOTES""#);
30243024
}
3025+
3026+
#[test]
3027+
fn test_sql_keywords_as_select_item_aliases() {
3028+
// Some keywords that should be parsed as an alias
3029+
let unreserved_kws = vec!["CLUSTER", "FETCH", "RETURNING", "LIMIT", "EXCEPT"];
3030+
for kw in unreserved_kws {
3031+
snowflake()
3032+
.one_statement_parses_to(&format!("SELECT 1 {kw}"), &format!("SELECT 1 AS {kw}"));
3033+
}
3034+
3035+
// Some keywords that should not be parsed as an alias
3036+
let reserved_kws = vec![
3037+
"FROM",
3038+
"GROUP",
3039+
"HAVING",
3040+
"INTERSECT",
3041+
"INTO",
3042+
"ORDER",
3043+
"SELECT",
3044+
"UNION",
3045+
"WHERE",
3046+
"WITH",
3047+
];
3048+
for kw in reserved_kws {
3049+
assert!(snowflake()
3050+
.parse_sql_statements(&format!("SELECT 1 {kw}"))
3051+
.is_err());
3052+
}
3053+
}

0 commit comments

Comments
 (0)