From 5eaab33b428609f5545a11170dfd6909385283f4 Mon Sep 17 00:00:00 2001 From: Yoav Cohen Date: Sun, 13 Jul 2025 16:36:43 +0200 Subject: [PATCH 1/2] Improve accuracy of lookahead in implicit LIMIT alias --- src/dialect/snowflake.rs | 21 ++++++++++++++++++--- tests/sqlparser_snowflake.rs | 5 +++++ 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/src/dialect/snowflake.rs b/src/dialect/snowflake.rs index fcf94ee75..b3dc14358 100644 --- a/src/dialect/snowflake.rs +++ b/src/dialect/snowflake.rs @@ -351,8 +351,7 @@ impl Dialect for SnowflakeDialect { match kw { // The following keywords can be considered an alias as long as // they are not followed by other tokens that may change their meaning - Keyword::LIMIT - | Keyword::RETURNING + Keyword::RETURNING | Keyword::INNER | Keyword::USING | Keyword::PIVOT @@ -365,6 +364,18 @@ impl Dialect for SnowflakeDialect { false } + // `LIMIT` can be considered an alias as long as it's not followed by a value. For example: + // `SELECT * FROM tbl LIMIT WHERE 1=1` - alias + // `SELECT * FROM tbl LIMIT 3` - not an alias + Keyword::LIMIT + if matches!( + parser.peek_token().token, + Token::Number(_, _) | Token::Placeholder(_) + ) => + { + false + } + // `FETCH` can be considered an alias as long as it's not followed by `FIRST`` or `NEXT` // which would give it a different meanings, for example: // `SELECT * FROM tbl FETCH FIRST 10 ROWS` - not an alias @@ -373,7 +384,10 @@ impl Dialect for SnowflakeDialect { if parser .peek_one_of_keywords(&[Keyword::FIRST, Keyword::NEXT]) .is_some() - || matches!(parser.peek_token().token, Token::Number(_, _)) => + || matches!( + parser.peek_token().token, + Token::Number(_, _) | Token::Placeholder(_) + ) => { false } @@ -387,6 +401,7 @@ impl Dialect for SnowflakeDialect { { false } + Keyword::GLOBAL if parser.peek_keyword(Keyword::FULL) => false, // Reserved keywords by the Snowflake dialect, which seem to be less strictive diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index 562ddfea7..00764dd3f 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -3586,6 +3586,11 @@ fn test_sql_keywords_as_table_aliases() { .parse_sql_statements(&format!("SELECT * FROM tbl {kw}")) .is_err()); } + + // LIMIT as alias and not as alias + snowflake().one_statement_parses_to("SELECT * FROM tbl LIMIT", "SELECT * FROM tbl AS LIMIT"); + snowflake().verified_stmt("SELECT * FROM tbl LIMIT 1"); + snowflake().verified_stmt("SELECT * FROM tbl LIMIT $1"); } #[test] From 72402690ce5204103f7882fc7797062b0d0c7fd6 Mon Sep 17 00:00:00 2001 From: Yoav Cohen Date: Wed, 16 Jul 2025 07:55:43 +0300 Subject: [PATCH 2/2] Improve the look ahead accuracy --- src/dialect/snowflake.rs | 41 ++++++++++++++++++------------------ tests/sqlparser_snowflake.rs | 15 ++++++++++++- 2 files changed, 35 insertions(+), 21 deletions(-) diff --git a/src/dialect/snowflake.rs b/src/dialect/snowflake.rs index b3dc14358..baf99b84b 100644 --- a/src/dialect/snowflake.rs +++ b/src/dialect/snowflake.rs @@ -23,8 +23,8 @@ use crate::ast::helpers::stmt_data_loading::{ FileStagingCommand, StageLoadSelectItem, StageLoadSelectItemKind, StageParamsObject, }; use crate::ast::{ - ColumnOption, ColumnPolicy, ColumnPolicyProperty, CopyIntoSnowflakeKind, Ident, - IdentityParameters, IdentityProperty, IdentityPropertyFormatKind, IdentityPropertyKind, + ColumnOption, ColumnPolicy, ColumnPolicyProperty, CopyIntoSnowflakeKind, DollarQuotedString, + Ident, IdentityParameters, IdentityProperty, IdentityPropertyFormatKind, IdentityPropertyKind, IdentityPropertyOrder, ObjectName, ObjectNamePart, RowAccessPolicy, ShowObjects, SqlOption, Statement, TagsColumnOption, WrappedCollection, }; @@ -307,22 +307,22 @@ impl Dialect for SnowflakeDialect { // they are not followed by other tokens that may change their meaning // e.g. `SELECT * EXCEPT (col1) FROM tbl` Keyword::EXCEPT - // e.g. `SELECT 1 LIMIT 5` - | Keyword::LIMIT - // e.g. `SELECT 1 OFFSET 5 ROWS` - | Keyword::OFFSET // e.g. `INSERT INTO t SELECT 1 RETURNING *` | Keyword::RETURNING if !matches!(parser.peek_token_ref().token, Token::Comma | Token::EOF) => { false } + // e.g. `SELECT 1 LIMIT 5` - not an alias + // e.g. `SELECT 1 OFFSET 5 ROWS` - not an alias + Keyword::LIMIT | Keyword::OFFSET if peek_for_limit_options(parser) => false, + // `FETCH` can be considered an alias as long as it's not followed by `FIRST`` or `NEXT` // which would give it a different meanings, for example: // `SELECT 1 FETCH FIRST 10 ROWS` - not an alias // `SELECT 1 FETCH 10` - not an alias Keyword::FETCH if parser.peek_one_of_keywords(&[Keyword::FIRST, Keyword::NEXT]).is_some() - || matches!(parser.peek_token().token, Token::Number(_, _)) => + || peek_for_limit_options(parser) => { false } @@ -358,7 +358,6 @@ impl Dialect for SnowflakeDialect { | Keyword::UNPIVOT | Keyword::EXCEPT | Keyword::MATCH_RECOGNIZE - | Keyword::OFFSET if !matches!(parser.peek_token_ref().token, Token::SemiColon | Token::EOF) => { false @@ -367,14 +366,7 @@ impl Dialect for SnowflakeDialect { // `LIMIT` can be considered an alias as long as it's not followed by a value. For example: // `SELECT * FROM tbl LIMIT WHERE 1=1` - alias // `SELECT * FROM tbl LIMIT 3` - not an alias - Keyword::LIMIT - if matches!( - parser.peek_token().token, - Token::Number(_, _) | Token::Placeholder(_) - ) => - { - false - } + Keyword::LIMIT | Keyword::OFFSET if peek_for_limit_options(parser) => false, // `FETCH` can be considered an alias as long as it's not followed by `FIRST`` or `NEXT` // which would give it a different meanings, for example: @@ -384,10 +376,7 @@ impl Dialect for SnowflakeDialect { if parser .peek_one_of_keywords(&[Keyword::FIRST, Keyword::NEXT]) .is_some() - || matches!( - parser.peek_token().token, - Token::Number(_, _) | Token::Placeholder(_) - ) => + || peek_for_limit_options(parser) => { false } @@ -487,6 +476,18 @@ impl Dialect for SnowflakeDialect { } } +// Peeks ahead to identify tokens that are expected after +// a LIMIT/FETCH keyword. +fn peek_for_limit_options(parser: &Parser) -> bool { + match &parser.peek_token_ref().token { + Token::Number(_, _) | Token::Placeholder(_) => true, + Token::SingleQuotedString(val) if val.is_empty() => true, + Token::DollarQuotedString(DollarQuotedString { value, .. }) if value.is_empty() => true, + Token::Word(w) if w.keyword == Keyword::NULL => true, + _ => false, + } +} + fn parse_file_staging_command(kw: Keyword, parser: &mut Parser) -> Result { let stage = parse_snowflake_stage_name(parser)?; let pattern = if parser.parse_keyword(Keyword::PATTERN) { diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index 00764dd3f..a7a633152 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -3535,6 +3535,15 @@ fn test_sql_keywords_as_select_item_aliases() { .parse_sql_statements(&format!("SELECT 1 {kw}")) .is_err()); } + + // LIMIT is alias + snowflake().one_statement_parses_to("SELECT 1 LIMIT", "SELECT 1 AS LIMIT"); + // LIMIT is not an alias + snowflake().verified_stmt("SELECT 1 LIMIT 1"); + snowflake().verified_stmt("SELECT 1 LIMIT $1"); + snowflake().verified_stmt("SELECT 1 LIMIT ''"); + snowflake().verified_stmt("SELECT 1 LIMIT NULL"); + snowflake().verified_stmt("SELECT 1 LIMIT $$$$"); } #[test] @@ -3587,10 +3596,14 @@ fn test_sql_keywords_as_table_aliases() { .is_err()); } - // LIMIT as alias and not as alias + // LIMIT is alias snowflake().one_statement_parses_to("SELECT * FROM tbl LIMIT", "SELECT * FROM tbl AS LIMIT"); + // LIMIT is not an alias snowflake().verified_stmt("SELECT * FROM tbl LIMIT 1"); snowflake().verified_stmt("SELECT * FROM tbl LIMIT $1"); + snowflake().verified_stmt("SELECT * FROM tbl LIMIT ''"); + snowflake().verified_stmt("SELECT * FROM tbl LIMIT NULL"); + snowflake().verified_stmt("SELECT * FROM tbl LIMIT $$$$"); } #[test]