Skip to content

Commit 61010ee

Browse files
committed
Improve support for reserved keywords as table aliases in Snowflake
1 parent 93450cc commit 61010ee

File tree

4 files changed

+162
-9
lines changed

4 files changed

+162
-9
lines changed

src/dialect/mod.rs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -992,11 +992,17 @@ pub trait Dialect: Debug + Any {
992992
explicit || self.is_column_alias(kw, parser)
993993
}
994994

995+
/// Returns true if the specified keyword should be parsed as a table identifier.
996+
/// See [keywords::RESERVED_FOR_TABLE_ALIAS]
997+
fn is_table_alias(&self, kw: &Keyword, _parser: &mut Parser) -> bool {
998+
!keywords::RESERVED_FOR_TABLE_ALIAS.contains(kw)
999+
}
1000+
9951001
/// Returns true if the specified keyword should be parsed as a table factor alias.
9961002
/// When explicit is true, the keyword is preceded by an `AS` word. Parser is provided
9971003
/// to enable looking ahead if needed.
998-
fn is_table_factor_alias(&self, explicit: bool, kw: &Keyword, _parser: &mut Parser) -> bool {
999-
explicit || !keywords::RESERVED_FOR_TABLE_ALIAS.contains(kw)
1004+
fn is_table_factor_alias(&self, explicit: bool, kw: &Keyword, parser: &mut Parser) -> bool {
1005+
explicit || self.is_table_alias(kw, parser)
10001006
}
10011007

10021008
/// Returns true if this dialect supports querying historical table data

src/dialect/snowflake.rs

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -345,6 +345,85 @@ impl Dialect for SnowflakeDialect {
345345
}
346346
}
347347

348+
fn is_table_alias(&self, kw: &Keyword, parser: &mut Parser) -> bool {
349+
match kw {
350+
// The following keywords can be considered an alias as long as
351+
// they are not followed by other tokens that may change their meaning
352+
Keyword::LIMIT
353+
| Keyword::RETURNING
354+
| Keyword::INNER
355+
| Keyword::USING
356+
| Keyword::PIVOT
357+
| Keyword::UNPIVOT
358+
| Keyword::EXCEPT
359+
| Keyword::MATCH_RECOGNIZE
360+
| Keyword::OFFSET
361+
if !matches!(parser.peek_token_ref().token, Token::SemiColon | Token::EOF) =>
362+
{
363+
false
364+
}
365+
366+
// `FETCH` can be considered an alias as long as it's not followed by `FIRST`` or `NEXT`
367+
// which would give it a different meanings, for example:
368+
// `SELECT * FROM tbl FETCH FIRST 10 ROWS` - not an alias
369+
// `SELECT * FROM tbl FETCH 10` - not an alias
370+
Keyword::FETCH
371+
if parser.peek_keyword(Keyword::FIRST)
372+
|| parser.peek_keyword(Keyword::NEXT)
373+
|| matches!(parser.peek_token().token, Token::Number(_, _)) =>
374+
{
375+
false
376+
}
377+
378+
// All sorts of join-related keywords can be considered aliases unless additional
379+
// keywords change their meaning.
380+
Keyword::RIGHT | Keyword::LEFT | Keyword::SEMI | Keyword::ANTI
381+
if parser
382+
.peek_one_of_keywords(&[Keyword::JOIN, Keyword::OUTER])
383+
.is_some() =>
384+
{
385+
false
386+
}
387+
Keyword::GLOBAL if parser.peek_keyword(Keyword::FULL) => false,
388+
389+
// Reserved keywords by the Snowflake dialect, which seem to be less strictive
390+
// than what is listed in `keywords::RESERVED_FOR_TABLE_ALIAS`. The following
391+
// keywords were tested with the this statement: `SELECT <KW>.* FROM tbl <KW>`.
392+
Keyword::WITH
393+
| Keyword::ORDER
394+
| Keyword::SELECT
395+
| Keyword::WHERE
396+
| Keyword::GROUP
397+
| Keyword::HAVING
398+
| Keyword::LATERAL
399+
| Keyword::UNION
400+
| Keyword::INTERSECT
401+
| Keyword::MINUS
402+
| Keyword::ON
403+
| Keyword::JOIN
404+
| Keyword::INNER
405+
| Keyword::CROSS
406+
| Keyword::FULL
407+
| Keyword::LEFT
408+
| Keyword::RIGHT
409+
| Keyword::NATURAL
410+
| Keyword::USING
411+
| Keyword::ASOF
412+
| Keyword::MATCH_CONDITION
413+
| Keyword::SET
414+
| Keyword::QUALIFY
415+
| Keyword::FOR
416+
| Keyword::START
417+
| Keyword::CONNECT
418+
| Keyword::SAMPLE
419+
| Keyword::TABLESAMPLE
420+
| Keyword::FROM => false,
421+
422+
// Any other word is considered an alias
423+
_ => true,
424+
}
425+
}
426+
348427
/// See: <https://docs.snowflake.com/en/sql-reference/constructs/at-before>
349428
fn supports_timestamp_versioning(&self) -> bool {
350429
true

tests/sqlparser_common.rs

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5544,7 +5544,8 @@ fn parse_named_window_functions() {
55445544
WINDOW w AS (PARTITION BY x), win AS (ORDER BY y)";
55455545
supported_dialects.verified_stmt(sql);
55465546

5547-
let select = verified_only_select(sql);
5547+
let select = all_dialects_except(|d| d.is_table_alias(&Keyword::WINDOW, &mut Parser::new(d)))
5548+
.verified_only_select(sql);
55485549

55495550
const EXPECTED_PROJ_QTY: usize = 2;
55505551
assert_eq!(EXPECTED_PROJ_QTY, select.projection.len());
@@ -5574,6 +5575,7 @@ fn parse_named_window_functions() {
55745575

55755576
#[test]
55765577
fn parse_window_clause() {
5578+
let dialects = all_dialects_except(|d| d.is_table_alias(&Keyword::WINDOW, &mut Parser::new(d)));
55775579
let sql = "SELECT * \
55785580
FROM mytable \
55795581
WINDOW \
@@ -5586,10 +5588,14 @@ fn parse_window_clause() {
55865588
window7 AS (window1 ROWS UNBOUNDED PRECEDING), \
55875589
window8 AS (window1 PARTITION BY a ORDER BY b ROWS UNBOUNDED PRECEDING) \
55885590
ORDER BY C3";
5589-
verified_only_select(sql);
5591+
dialects.verified_only_select(sql);
55905592

55915593
let sql = "SELECT * from mytable WINDOW window1 AS window2";
5592-
let dialects = all_dialects_except(|d| d.is::<BigQueryDialect>() || d.is::<GenericDialect>());
5594+
let dialects = all_dialects_except(|d| {
5595+
d.is::<BigQueryDialect>()
5596+
|| d.is::<GenericDialect>()
5597+
|| d.is_table_alias(&Keyword::WINDOW, &mut Parser::new(d))
5598+
});
55935599
let res = dialects.parse_sql_statements(sql);
55945600
assert_eq!(
55955601
ParserError::ParserError("Expected: (, found: window2".to_string()),
@@ -5599,14 +5605,15 @@ fn parse_window_clause() {
55995605

56005606
#[test]
56015607
fn test_parse_named_window() {
5608+
let dialects = all_dialects_except(|d| d.is_table_alias(&Keyword::WINDOW, &mut Parser::new(d)));
56025609
let sql = "SELECT \
56035610
MIN(c12) OVER window1 AS min1, \
56045611
MAX(c12) OVER window2 AS max1 \
56055612
FROM aggregate_test_100 \
56065613
WINDOW window1 AS (ORDER BY C12), \
56075614
window2 AS (PARTITION BY C11) \
56085615
ORDER BY C3";
5609-
let actual_select_only = verified_only_select(sql);
5616+
let actual_select_only = dialects.verified_only_select(sql);
56105617
let expected = Select {
56115618
select_token: AttachedToken::empty(),
56125619
distinct: None,
@@ -5755,14 +5762,18 @@ fn test_parse_named_window() {
57555762

57565763
#[test]
57575764
fn parse_window_and_qualify_clause() {
5765+
let dialects = all_dialects_except(|d| {
5766+
d.is_table_alias(&Keyword::WINDOW, &mut Parser::new(d))
5767+
|| d.is_table_alias(&Keyword::QUALIFY, &mut Parser::new(d))
5768+
});
57585769
let sql = "SELECT \
57595770
MIN(c12) OVER window1 AS min1 \
57605771
FROM aggregate_test_100 \
57615772
QUALIFY ROW_NUMBER() OVER my_window \
57625773
WINDOW window1 AS (ORDER BY C12), \
57635774
window2 AS (PARTITION BY C11) \
57645775
ORDER BY C3";
5765-
verified_only_select(sql);
5776+
dialects.verified_only_select(sql);
57665777

57675778
let sql = "SELECT \
57685779
MIN(c12) OVER window1 AS min1 \
@@ -5771,7 +5782,7 @@ fn parse_window_and_qualify_clause() {
57715782
window2 AS (PARTITION BY C11) \
57725783
QUALIFY ROW_NUMBER() OVER my_window \
57735784
ORDER BY C3";
5774-
verified_only_select(sql);
5785+
dialects.verified_only_select(sql);
57755786
}
57765787

57775788
#[test]
@@ -7439,7 +7450,8 @@ fn parse_join_syntax_variants() {
74397450
"SELECT c1 FROM t1 FULL JOIN t2 USING(c1)",
74407451
);
74417452

7442-
let res = parse_sql_statements("SELECT * FROM a OUTER JOIN b ON 1");
7453+
let dialects = all_dialects_except(|d| d.is_table_alias(&Keyword::OUTER, &mut Parser::new(d)));
7454+
let res = dialects.parse_sql_statements("SELECT * FROM a OUTER JOIN b ON 1");
74437455
assert_eq!(
74447456
ParserError::ParserError("Expected: APPLY, found: JOIN".to_string()),
74457457
res.unwrap_err()

tests/sqlparser_snowflake.rs

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3472,6 +3472,57 @@ fn test_sql_keywords_as_select_item_aliases() {
34723472
}
34733473
}
34743474

3475+
#[test]
3476+
fn test_sql_keywords_as_table_aliases() {
3477+
// Some keywords that should be parsed as an alias implicitly
3478+
let unreserved_kws = vec![
3479+
"VIEW",
3480+
"EXPLAIN",
3481+
"ANALYZE",
3482+
"SORT",
3483+
"PIVOT",
3484+
"UNPIVOT",
3485+
"TOP",
3486+
"LIMIT",
3487+
"OFFSET",
3488+
"FETCH",
3489+
"EXCEPT",
3490+
"CLUSTER",
3491+
"DISTRIBUTE",
3492+
"GLOBAL",
3493+
"ANTI",
3494+
"SEMI",
3495+
"RETURNING",
3496+
"OUTER",
3497+
"WINDOW",
3498+
"END",
3499+
"PARTITION",
3500+
"PREWHERE",
3501+
"SETTINGS",
3502+
"FORMAT",
3503+
"MATCH_RECOGNIZE",
3504+
"OPEN",
3505+
];
3506+
3507+
for kw in unreserved_kws {
3508+
snowflake().verified_stmt(&format!("SELECT * FROM tbl AS {kw}"));
3509+
snowflake().one_statement_parses_to(
3510+
&format!("SELECT * FROM tbl {kw}"),
3511+
&format!("SELECT * FROM tbl AS {kw}"),
3512+
);
3513+
}
3514+
3515+
// Some keywords that should not be parsed as an alias implicitly
3516+
let reserved_kws = vec![
3517+
"FROM", "GROUP", "HAVING", "ORDER", "SELECT", "UNION", "WHERE", "WITH",
3518+
];
3519+
for kw in reserved_kws {
3520+
assert!(snowflake()
3521+
.parse_sql_statements(&format!("SELECT * FROM tbl {kw}"))
3522+
.is_err());
3523+
}
3524+
}
3525+
34753526
#[test]
34763527
fn test_timetravel_at_before() {
34773528
snowflake().verified_only_select("SELECT * FROM tbl AT(TIMESTAMP => '2024-12-15 00:00:00')");
@@ -4232,3 +4283,8 @@ fn test_snowflake_create_view_with_composite_policy_name() {
42324283
r#"CREATE VIEW X (COL WITH MASKING POLICY foo.bar.baz) AS SELECT * FROM Y"#;
42334284
snowflake().verified_stmt(create_view_with_tag);
42344285
}
4286+
4287+
#[test]
4288+
fn test_x() {
4289+
snowflake().parse_sql_statements("SELECT foo FROM (SELECT * FROM bar OFFSET 2 ROWS FETCH FIRST 2 ROWS ONLY) OFFSET 2 ROWS FETCH FIRST 2 ROWS ONLY").unwrap();
4290+
}

0 commit comments

Comments
 (0)