From ebebcbad49cc0dea5d053010e08a7074f06d6947 Mon Sep 17 00:00:00 2001 From: LucaCappelletti94 Date: Wed, 23 Sep 2026 02:03:29 +0200 Subject: [PATCH 1/4] BigQuery: Allow tokens after a hyphenated identifier ending in a number --- src/parser/mod.rs | 7 +++---- tests/sqlparser_bigquery.rs | 22 ++++++++++++++++++++++ 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 3efb22a349..c1fbfa89ac 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -14474,11 +14474,10 @@ impl<'a> Parser<'a> { } } - // If the last segment was a number, we must check that it's followed by whitespace, - // otherwise foo-123a will be parsed as `foo-123` with the alias `a`. + // A word or number butted onto a trailing number glues into it, as in `foo-123a`. if requires_whitespace { - let token = self.next_token(); - if !matches!(token.token, Token::EOF | Token::Whitespace(_)) { + let token = self.peek_token_no_skip(); + if matches!(token.token, Token::Word(_) | Token::Number(_, false)) { return self .expected("whitespace following hyphenated identifier", token); } diff --git a/tests/sqlparser_bigquery.rs b/tests/sqlparser_bigquery.rs index b110423370..63b1099fa8 100644 --- a/tests/sqlparser_bigquery.rs +++ b/tests/sqlparser_bigquery.rs @@ -3001,3 +3001,25 @@ fn test_byte_and_raw_string_quote_escaping() { err ); } + +#[test] +fn parse_hyphenated_identifier_after_numeric_segment() { + bigquery().verified_stmt("SELECT * FROM project-1.dataset-2.table-3 WHERE x = 1"); + bigquery().verified_stmt("SELECT * FROM foo-1 AS t"); + bigquery().verified_stmt("SELECT * FROM foo-1 JOIN bar-2 USING(id)"); + bigquery().verified_stmt("SELECT * FROM foo-1, bar-2"); + bigquery().verified_stmt("DELETE FROM foo-1 WHERE a = 1"); + bigquery().verified_stmt("UPDATE foo-1 SET a = 1"); + bigquery().verified_stmt("CREATE TABLE t-1 (a INT64)"); + bigquery().verified_stmt("CREATE TABLE ship_mo-29 (a INT64)"); + + let err = bigquery() + .parse_sql_statements("SELECT * FROM foo-123a") + .unwrap_err(); + assert_eq!( + ParserError::ParserError( + "Expected: whitespace following hyphenated identifier, found: a".to_string(), + ), + err + ); +} From 286ac975c4d2ac96ffe034f098ef37d6ce1fa1ce Mon Sep 17 00:00:00 2001 From: LucaCappelletti94 Date: Tue, 20 Oct 2026 17:56:32 +0200 Subject: [PATCH 2/4] Cover BigQuery hyphenated table boundaries --- .../bigquery_2620_hyphen_numeric_boundary.sql | 5 +++++ tests/sqlparser_bigquery.rs | 13 +++++++++++++ 2 files changed, 18 insertions(+) create mode 100644 fuzz/fuzz_seeds/bigquery_2620_hyphen_numeric_boundary.sql diff --git a/fuzz/fuzz_seeds/bigquery_2620_hyphen_numeric_boundary.sql b/fuzz/fuzz_seeds/bigquery_2620_hyphen_numeric_boundary.sql new file mode 100644 index 0000000000..568b8e18b1 --- /dev/null +++ b/fuzz/fuzz_seeds/bigquery_2620_hyphen_numeric_boundary.sql @@ -0,0 +1,5 @@ +SELECT * FROM project-1.dataset-2.table-3 WHERE x = 1; +SELECT * FROM foo-1; +SELECT * FROM (SELECT 1 FROM foo-1); +SELECT * FROM foo-1, bar-2; +CREATE TABLE t-1 (a INT64) \ No newline at end of file diff --git a/tests/sqlparser_bigquery.rs b/tests/sqlparser_bigquery.rs index 63b1099fa8..5af998bded 100644 --- a/tests/sqlparser_bigquery.rs +++ b/tests/sqlparser_bigquery.rs @@ -3012,6 +3012,9 @@ fn parse_hyphenated_identifier_after_numeric_segment() { bigquery().verified_stmt("UPDATE foo-1 SET a = 1"); bigquery().verified_stmt("CREATE TABLE t-1 (a INT64)"); bigquery().verified_stmt("CREATE TABLE ship_mo-29 (a INT64)"); + bigquery().one_statement_parses_to("SELECT * FROM foo-1;", "SELECT * FROM foo-1"); + bigquery().verified_stmt("SELECT * FROM (SELECT 1 FROM foo-1)"); + bigquery().one_statement_parses_to("CREATE TABLE t-1(a INT64)", "CREATE TABLE t-1 (a INT64)"); let err = bigquery() .parse_sql_statements("SELECT * FROM foo-123a") @@ -3022,4 +3025,14 @@ fn parse_hyphenated_identifier_after_numeric_segment() { ), err ); + + let err = bigquery() + .parse_sql_statements("SELECT * FROM foo-1`x`") + .unwrap_err(); + assert_eq!( + ParserError::ParserError( + "Expected: whitespace following hyphenated identifier, found: `x`".to_string(), + ), + err + ); } From 36bc9a71aa3b54cf0e439e235a4abf2f8ba26b44 Mon Sep 17 00:00:00 2001 From: LucaCappelletti94 Date: Fri, 23 Oct 2026 23:57:16 +0200 Subject: [PATCH 3/4] Place regression tests beside related syntax cases --- tests/sqlparser_bigquery.rs | 70 ++++++++++++++++++------------------- 1 file changed, 35 insertions(+), 35 deletions(-) diff --git a/tests/sqlparser_bigquery.rs b/tests/sqlparser_bigquery.rs index 5af998bded..15a08e4de4 100644 --- a/tests/sqlparser_bigquery.rs +++ b/tests/sqlparser_bigquery.rs @@ -1747,6 +1747,41 @@ fn parse_hyphenated_table_identifiers() { ); } +#[test] +fn parse_hyphenated_identifier_after_numeric_segment() { + bigquery().verified_stmt("SELECT * FROM project-1.dataset-2.table-3 WHERE x = 1"); + bigquery().verified_stmt("SELECT * FROM foo-1 AS t"); + bigquery().verified_stmt("SELECT * FROM foo-1 JOIN bar-2 USING(id)"); + bigquery().verified_stmt("SELECT * FROM foo-1, bar-2"); + bigquery().verified_stmt("DELETE FROM foo-1 WHERE a = 1"); + bigquery().verified_stmt("UPDATE foo-1 SET a = 1"); + bigquery().verified_stmt("CREATE TABLE t-1 (a INT64)"); + bigquery().verified_stmt("CREATE TABLE ship_mo-29 (a INT64)"); + bigquery().one_statement_parses_to("SELECT * FROM foo-1;", "SELECT * FROM foo-1"); + bigquery().verified_stmt("SELECT * FROM (SELECT 1 FROM foo-1)"); + bigquery().one_statement_parses_to("CREATE TABLE t-1(a INT64)", "CREATE TABLE t-1 (a INT64)"); + + let err = bigquery() + .parse_sql_statements("SELECT * FROM foo-123a") + .unwrap_err(); + assert_eq!( + ParserError::ParserError( + "Expected: whitespace following hyphenated identifier, found: a".to_string(), + ), + err + ); + + let err = bigquery() + .parse_sql_statements("SELECT * FROM foo-1`x`") + .unwrap_err(); + assert_eq!( + ParserError::ParserError( + "Expected: whitespace following hyphenated identifier, found: `x`".to_string(), + ), + err + ); +} + #[test] fn parse_table_time_travel() { let version = "2023-08-18 23:08:18".to_string(); @@ -3001,38 +3036,3 @@ fn test_byte_and_raw_string_quote_escaping() { err ); } - -#[test] -fn parse_hyphenated_identifier_after_numeric_segment() { - bigquery().verified_stmt("SELECT * FROM project-1.dataset-2.table-3 WHERE x = 1"); - bigquery().verified_stmt("SELECT * FROM foo-1 AS t"); - bigquery().verified_stmt("SELECT * FROM foo-1 JOIN bar-2 USING(id)"); - bigquery().verified_stmt("SELECT * FROM foo-1, bar-2"); - bigquery().verified_stmt("DELETE FROM foo-1 WHERE a = 1"); - bigquery().verified_stmt("UPDATE foo-1 SET a = 1"); - bigquery().verified_stmt("CREATE TABLE t-1 (a INT64)"); - bigquery().verified_stmt("CREATE TABLE ship_mo-29 (a INT64)"); - bigquery().one_statement_parses_to("SELECT * FROM foo-1;", "SELECT * FROM foo-1"); - bigquery().verified_stmt("SELECT * FROM (SELECT 1 FROM foo-1)"); - bigquery().one_statement_parses_to("CREATE TABLE t-1(a INT64)", "CREATE TABLE t-1 (a INT64)"); - - let err = bigquery() - .parse_sql_statements("SELECT * FROM foo-123a") - .unwrap_err(); - assert_eq!( - ParserError::ParserError( - "Expected: whitespace following hyphenated identifier, found: a".to_string(), - ), - err - ); - - let err = bigquery() - .parse_sql_statements("SELECT * FROM foo-1`x`") - .unwrap_err(); - assert_eq!( - ParserError::ParserError( - "Expected: whitespace following hyphenated identifier, found: `x`".to_string(), - ), - err - ); -} From a9072311b000aec304803fd2fc024b292ce976ad Mon Sep 17 00:00:00 2001 From: LucaCappelletti94 Date: Sat, 24 Oct 2026 13:34:23 +0200 Subject: [PATCH 4/4] Allow quoted aliases after BigQuery numeric hyphen segments --- .../bigquery_2620_hyphen_numeric_boundary.sql | 3 ++- src/parser/mod.rs | 12 ++++++++-- tests/sqlparser_bigquery.rs | 24 +++++++++++-------- 3 files changed, 26 insertions(+), 13 deletions(-) diff --git a/fuzz/fuzz_seeds/bigquery_2620_hyphen_numeric_boundary.sql b/fuzz/fuzz_seeds/bigquery_2620_hyphen_numeric_boundary.sql index 568b8e18b1..38a03eeac3 100644 --- a/fuzz/fuzz_seeds/bigquery_2620_hyphen_numeric_boundary.sql +++ b/fuzz/fuzz_seeds/bigquery_2620_hyphen_numeric_boundary.sql @@ -2,4 +2,5 @@ SELECT * FROM project-1.dataset-2.table-3 WHERE x = 1; SELECT * FROM foo-1; SELECT * FROM (SELECT 1 FROM foo-1); SELECT * FROM foo-1, bar-2; -CREATE TABLE t-1 (a INT64) \ No newline at end of file +CREATE TABLE t-1 (a INT64); +SELECT * FROM foo-1`x`; \ No newline at end of file diff --git a/src/parser/mod.rs b/src/parser/mod.rs index c1fbfa89ac..9a7b86d543 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -14419,6 +14419,8 @@ impl<'a> Parser<'a> { /// with a digit. Subsequent segments are either must either be valid identifiers or /// integers, e.g. foo-123 is allowed, but foo-123a is not. /// + /// A quoted alias may immediately follow the identifier. + /// /// [BigQuery-lexical](https://cloud.google.com/bigquery/docs/reference/standard-sql/lexical) /// /// Return a tuple of the identifier and a boolean indicating it ends with a period. @@ -14474,10 +14476,16 @@ impl<'a> Parser<'a> { } } - // A word or number butted onto a trailing number glues into it, as in `foo-123a`. + // An unquoted word or number can extend a trailing number, as in `foo-123a`. if requires_whitespace { let token = self.peek_token_no_skip(); - if matches!(token.token, Token::Word(_) | Token::Number(_, false)) { + if matches!( + token.token, + Token::Word(Word { + quote_style: None, + .. + }) | Token::Number(_, false) + ) { return self .expected("whitespace following hyphenated identifier", token); } diff --git a/tests/sqlparser_bigquery.rs b/tests/sqlparser_bigquery.rs index 15a08e4de4..78518a0fa0 100644 --- a/tests/sqlparser_bigquery.rs +++ b/tests/sqlparser_bigquery.rs @@ -1770,16 +1770,6 @@ fn parse_hyphenated_identifier_after_numeric_segment() { ), err ); - - let err = bigquery() - .parse_sql_statements("SELECT * FROM foo-1`x`") - .unwrap_err(); - assert_eq!( - ParserError::ParserError( - "Expected: whitespace following hyphenated identifier, found: `x`".to_string(), - ), - err - ); } #[test] @@ -3036,3 +3026,17 @@ fn test_byte_and_raw_string_quote_escaping() { err ); } + +#[test] +fn parse_table_identifier_with_adjacent_quoted_alias() { + for table in ["foo", "foo-bar", "foo-1"] { + let sql = format!("SELECT * FROM {table}`x`"); + let canonical = format!("SELECT * FROM {table} `x`"); + let select = bigquery().verified_only_select_with_canonical(&sql, canonical.as_str()); + let TableFactor::Table { name, alias, .. } = &select.from[0].relation else { + panic!("Expected a table"); + }; + assert_eq!(name, &ObjectName::from(vec![Ident::new(table)])); + assert_eq!(alias.as_ref().unwrap().name, Ident::with_quote('`', "x")); + } +}