From 089047894abf802810a72c192a702bfa96670505 Mon Sep 17 00:00:00 2001 From: LucaCappelletti94 Date: Tue, 13 Oct 2026 14:55:52 +0200 Subject: [PATCH 1/6] Reject unquoted reserved keywords as table names --- src/dialect/clickhouse.rs | 8 ++++++++ src/dialect/databricks.rs | 8 ++++++++ src/dialect/hive.rs | 8 ++++++++ src/dialect/mssql.rs | 8 +++++++- src/dialect/oracle.rs | 11 ++++++++++- src/keywords.rs | 8 +++++--- src/parser/mod.rs | 10 ++++++++++ tests/sqlparser_clickhouse.rs | 8 ++++++++ tests/sqlparser_common.rs | 26 ++++++++++++++++++++++++-- tests/sqlparser_databricks.rs | 6 ++++++ tests/sqlparser_hive.rs | 6 ++++++ tests/sqlparser_mssql.rs | 6 ++++++ tests/sqlparser_oracle.rs | 6 ++++++ 13 files changed, 112 insertions(+), 7 deletions(-) diff --git a/src/dialect/clickhouse.rs b/src/dialect/clickhouse.rs index 41a0e8be42..e670db211c 100644 --- a/src/dialect/clickhouse.rs +++ b/src/dialect/clickhouse.rs @@ -16,6 +16,8 @@ // under the License. use crate::dialect::Dialect; +use crate::keywords::Keyword; +use crate::parser::Parser; /// A [`Dialect`] for [ClickHouse](https://clickhouse.com/). #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] @@ -45,6 +47,12 @@ impl Dialect for ClickHouseDialect { self.is_identifier_start(ch) || ch.is_ascii_digit() || ch == '$' } + /// ClickHouse keywords are not reserved, so any of them can be a table name. + /// See + fn is_table_factor(&self, _kw: &Keyword, _parser: &mut Parser) -> bool { + true + } + /// ClickHouse accepts both backticks and double quotes, but its own /// formatter emits backticks by default. /// See diff --git a/src/dialect/databricks.rs b/src/dialect/databricks.rs index a88cc8d7d7..f973845f39 100644 --- a/src/dialect/databricks.rs +++ b/src/dialect/databricks.rs @@ -16,6 +16,8 @@ // under the License. use crate::dialect::Dialect; +use crate::keywords::{Keyword, RESERVED_FOR_TABLE_FACTOR}; +use crate::parser::Parser; /// A [`Dialect`] for [Databricks SQL](https://www.databricks.com/) /// @@ -57,6 +59,12 @@ impl Dialect for DatabricksDialect { matches!(ch, 'a'..='z' | 'A'..='Z' | '0'..='9' | '_') } + /// LIMIT is not a reserved word in Databricks SQL. + /// See + fn is_table_factor(&self, kw: &Keyword, _parser: &mut Parser) -> bool { + kw == &Keyword::LIMIT || !RESERVED_FOR_TABLE_FACTOR.contains(kw) + } + fn supports_numeric_prefix(&self) -> bool { true } diff --git a/src/dialect/hive.rs b/src/dialect/hive.rs index 575219e7b2..e80bc75d2b 100644 --- a/src/dialect/hive.rs +++ b/src/dialect/hive.rs @@ -16,6 +16,8 @@ // under the License. use crate::dialect::Dialect; +use crate::keywords::{Keyword, RESERVED_FOR_TABLE_FACTOR}; +use crate::parser::Parser; /// A [`Dialect`] for [Hive](https://hive.apache.org/). #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] @@ -62,6 +64,12 @@ impl Dialect for HiveDialect { || ch == '}' } + /// LIMIT is not reserved in Hive; identifiers may be nonReserved keywords. + /// See + fn is_table_factor(&self, kw: &Keyword, _parser: &mut Parser) -> bool { + kw == &Keyword::LIMIT || !RESERVED_FOR_TABLE_FACTOR.contains(kw) + } + fn supports_filter_during_aggregation(&self) -> bool { true } diff --git a/src/dialect/mssql.rs b/src/dialect/mssql.rs index 06a07b61d8..9808eec0fd 100644 --- a/src/dialect/mssql.rs +++ b/src/dialect/mssql.rs @@ -21,7 +21,7 @@ use crate::ast::{ GranteesType, IfStatement, Statement, }; use crate::dialect::Dialect; -use crate::keywords::Keyword; +use crate::keywords::{Keyword, RESERVED_FOR_TABLE_FACTOR}; use crate::parser::{Parser, ParserError}; use crate::tokenizer::Token; #[cfg(not(feature = "std"))] @@ -167,6 +167,12 @@ impl Dialect for MsSqlDialect { } } + /// LIMIT is not a reserved word in Transact-SQL. + /// See + fn is_table_factor(&self, kw: &Keyword, _parser: &mut Parser) -> bool { + kw == &Keyword::LIMIT || !RESERVED_FOR_TABLE_FACTOR.contains(kw) + } + fn is_table_factor_alias(&self, explicit: bool, kw: &Keyword, parser: &mut Parser) -> bool { match kw { // List of keywords that cannot be used as table aliases in MSSQL diff --git a/src/dialect/oracle.rs b/src/dialect/oracle.rs index ccbef5b621..40c91cea35 100644 --- a/src/dialect/oracle.rs +++ b/src/dialect/oracle.rs @@ -22,7 +22,10 @@ use crate::{ tokenizer::Token, }; -use super::{keywords::Keyword, Dialect, Precedence}; +use super::{ + keywords::{Keyword, RESERVED_FOR_TABLE_FACTOR}, + Dialect, Precedence, +}; const RESERVED_KEYWORDS_FOR_SELECT_ITEM_OPERATOR: [Keyword; 1] = [Keyword::CONNECT_BY_ROOT]; @@ -49,6 +52,12 @@ impl Dialect for OracleDialect { ch.is_alphanumeric() || ch == '_' || ch == '$' || ch == '#' || ch == '@' } + /// LIMIT is not a reserved word in Oracle SQL. + /// See + fn is_table_factor(&self, kw: &Keyword, _parser: &mut Parser) -> bool { + kw == &Keyword::LIMIT || !RESERVED_FOR_TABLE_FACTOR.contains(kw) + } + fn supports_outer_join_operator(&self) -> bool { true } diff --git a/src/keywords.rs b/src/keywords.rs index bde973be3a..0ddc9173c8 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -1306,14 +1306,16 @@ pub const RESERVED_FOR_COLUMN_ALIAS: &[Keyword] = &[ Keyword::END, ]; -/// Global list of reserved keywords allowed after FROM. -/// Parser should call Dialect::get_reserved_keyword_after_from -/// to allow for each dialect to customize the list. +/// Global list of reserved keywords that cannot be parsed as a table +/// factor name without special handling like quoting. Parser should +/// call `Dialect::is_table_factor` to allow for each dialect to +/// customize the list. pub const RESERVED_FOR_TABLE_FACTOR: &[Keyword] = &[ Keyword::INTO, Keyword::LIMIT, Keyword::HAVING, Keyword::WHERE, + Keyword::ON, ]; /// Global list of reserved keywords that cannot be parsed as identifiers diff --git a/src/parser/mod.rs b/src/parser/mod.rs index c7f565586e..761d43d539 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -17095,6 +17095,16 @@ impl<'a> Parser<'a> { // Stage reference: @mystage or @namespace.stage (e.g. Snowflake) self.parse_snowflake_stage_table_factor() } else { + // Unquoted reserved keywords, e.g. `ON`, are not table names. + let peek_keyword = if let Token::Word(w) = &self.peek_token_ref().token { + w.keyword + } else { + Keyword::NoKeyword + }; + if !self.dialect.is_table_factor(&peek_keyword, self) { + return self.expected_ref("table name", self.peek_token_ref()); + } + let name = self.parse_object_name(true)?; let json_path = match &self.peek_token_ref().token { diff --git a/tests/sqlparser_clickhouse.rs b/tests/sqlparser_clickhouse.rs index 5b88aa0d7f..5865f312eb 100644 --- a/tests/sqlparser_clickhouse.rs +++ b/tests/sqlparser_clickhouse.rs @@ -2418,3 +2418,11 @@ fn parse_create_table_engine_name_quoting() { clickhouse_and_generic().verified_stmt("CREATE TABLE t (a INT) ENGINE = ``"); clickhouse().verified_stmt("CREATE TABLE t (a INT) ENGINE = MergeTree"); } + +#[test] +fn parse_non_reserved_keyword_table_names() { + for table_name in ["into", "limit", "having", "where", "on"] { + clickhouse().verified_stmt(&format!("SELECT * FROM {table_name}")); + clickhouse().verified_stmt(&format!("SELECT * FROM t JOIN {table_name} ON true")); + } +} diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index d3f2bf42de..285112f617 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -1258,10 +1258,10 @@ fn parse_select_wildcard() { #[test] fn parse_count_wildcard() { - verified_only_select("SELECT COUNT(*) FROM Order WHERE id = 10"); + verified_only_select("SELECT COUNT(*) FROM orders WHERE id = 10"); verified_only_select( - "SELECT COUNT(Employee.*) FROM Order JOIN Employee ON Order.employee = Employee.id", + "SELECT COUNT(Employee.*) FROM orders JOIN Employee ON orders.employee = Employee.id", ); } @@ -21319,6 +21319,21 @@ fn unary_prefix_operator_renders_apart_from_cast_operand() { } } +#[test] +fn on_keyword_rejected_as_table_name() { + let dialects = all_dialects_where(|d| !d.is_table_factor(&Keyword::ON, &mut Parser::new(d))); + for sql in [ + "SELECT * FROM ON", + "SELECT * FROM t JOIN ON", + "SELECT * FROM t JOIN ON u ON TRUE", + ] { + assert_eq!( + ParserError::ParserError("Expected: table name, found: ON".to_string()), + dialects.parse_sql_statements(sql).unwrap_err() + ); + } +} + #[test] fn parse_string_literal_alias() { for (sql, canonical, table, explicit, error) in [ @@ -21505,4 +21520,11 @@ fn parse_qualified_literal_prefix_requires_adjacent_period() { ] { dialects.tokenizes_to(sql, vec![token]); } + +#[test] +fn quoted_on_allowed_as_table_name() { + let dialects = all_dialects_where(|d| d.is_delimited_identifier_start('"')); + dialects.verified_stmt(r#"SELECT * FROM "ON""#); + let dialects = all_dialects_where(|d| d.is_delimited_identifier_start('`')); + dialects.verified_stmt("SELECT * FROM `ON`"); } diff --git a/tests/sqlparser_databricks.rs b/tests/sqlparser_databricks.rs index b9eb0288f7..e126c83c9b 100644 --- a/tests/sqlparser_databricks.rs +++ b/tests/sqlparser_databricks.rs @@ -784,3 +784,9 @@ fn parse_databricks_collated_data_types() { )) ); } + +#[test] +fn parse_non_reserved_keyword_table_names() { + databricks().verified_stmt("SELECT * FROM limit"); + databricks().verified_stmt("SELECT * FROM t JOIN limit ON true"); +} diff --git a/tests/sqlparser_hive.rs b/tests/sqlparser_hive.rs index 6459b8fc42..a9ed84cc67 100644 --- a/tests/sqlparser_hive.rs +++ b/tests/sqlparser_hive.rs @@ -576,3 +576,9 @@ fn hive() -> TestedDialects { fn hive_and_generic() -> TestedDialects { TestedDialects::new(vec![Box::new(HiveDialect {}), Box::new(GenericDialect {})]) } + +#[test] +fn parse_non_reserved_keyword_table_names() { + hive().verified_stmt("SELECT * FROM limit"); + hive().verified_stmt("SELECT * FROM t JOIN limit ON true"); +} diff --git a/tests/sqlparser_mssql.rs b/tests/sqlparser_mssql.rs index a42f216757..2fd8e13975 100644 --- a/tests/sqlparser_mssql.rs +++ b/tests/sqlparser_mssql.rs @@ -3013,3 +3013,9 @@ fn parse_mssql_declare_comma_list_display() { ms().verified_stmt("DECLARE @a INT = 1, @b TEXT = 'x'"); ms().verified_stmt("DECLARE @foo CURSOR, @bar INT"); } + +#[test] +fn parse_non_reserved_keyword_table_names() { + ms().verified_stmt("SELECT * FROM limit"); + ms().verified_stmt("SELECT * FROM t JOIN limit ON true"); +} diff --git a/tests/sqlparser_oracle.rs b/tests/sqlparser_oracle.rs index 888778e235..0ed573be68 100644 --- a/tests/sqlparser_oracle.rs +++ b/tests/sqlparser_oracle.rs @@ -539,3 +539,9 @@ fn test_insert_without_alias() { if matches!(&*source, Query { body, .. } if matches!(&**body, SetExpr::Values(_))) )); } + +#[test] +fn parse_non_reserved_keyword_table_names() { + oracle().verified_stmt("SELECT * FROM limit"); + oracle().verified_stmt("SELECT * FROM t JOIN limit ON TRUE"); +} From 160b47a22a54c88638382b0ab9c91d40973e44f4 Mon Sep 17 00:00:00 2001 From: LucaCappelletti94 Date: Fri, 16 Oct 2026 23:54:45 +0200 Subject: [PATCH 2/6] Expect the table name error in the pipe operator JOIN case --- tests/sqlparser_common.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 285112f617..4dab13b0f1 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -18415,7 +18415,7 @@ fn parse_pipeline_operator_negative_tests() { // Test that JOIN without table name fails ( "SELECT * FROM users |> JOIN ON users.id = orders.user_id", - "Expected: end of statement, found: .", + "Expected: table name, found: ON", ), // Test that CROSS JOIN with ON condition fails ( From 87a18592c557a47e3b335103137ecb73b064abd4 Mon Sep 17 00:00:00 2001 From: LucaCappelletti94 Date: Mon, 19 Oct 2026 20:10:23 +0200 Subject: [PATCH 3/6] Consume the keyword before asking is_table_factor at the table name --- src/parser/mod.rs | 16 ++++++++++------ tests/sqlparser_common.rs | 1 + 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 761d43d539..62a13ef653 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -17095,13 +17095,17 @@ impl<'a> Parser<'a> { // Stage reference: @mystage or @namespace.stage (e.g. Snowflake) self.parse_snowflake_stage_table_factor() } else { - // Unquoted reserved keywords, e.g. `ON`, are not table names. - let peek_keyword = if let Token::Word(w) = &self.peek_token_ref().token { - w.keyword - } else { - Keyword::NoKeyword + // Unquoted reserved keywords, e.g. `ON`, are not table names. The + // keyword is consumed first so that `is_table_factor` peeks past it, + // as it does at the trailing comma check. + self.advance_token(); + let keyword = match &self.get_current_token().token { + Token::Word(w) => w.keyword, + _ => Keyword::NoKeyword, }; - if !self.dialect.is_table_factor(&peek_keyword, self) { + let is_table_name = self.dialect.is_table_factor(&keyword, self); + self.prev_token(); + if !is_table_name { return self.expected_ref("table name", self.peek_token_ref()); } diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 4dab13b0f1..e1df4a9154 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -21520,6 +21520,7 @@ fn parse_qualified_literal_prefix_requires_adjacent_period() { ] { dialects.tokenizes_to(sql, vec![token]); } +} #[test] fn quoted_on_allowed_as_table_name() { From 63395734a67912408450a91936314ed5e69f17c2 Mon Sep 17 00:00:00 2001 From: LucaCappelletti94 Date: Sat, 24 Oct 2026 00:23:32 +0200 Subject: [PATCH 4/6] Place regression tests beside related syntax cases --- tests/sqlparser_clickhouse.rs | 16 ++++++------ tests/sqlparser_common.rs | 46 +++++++++++++++++------------------ tests/sqlparser_databricks.rs | 12 ++++----- tests/sqlparser_hive.rs | 12 ++++----- tests/sqlparser_mssql.rs | 12 ++++----- 5 files changed, 49 insertions(+), 49 deletions(-) diff --git a/tests/sqlparser_clickhouse.rs b/tests/sqlparser_clickhouse.rs index 5865f312eb..2c59d66cad 100644 --- a/tests/sqlparser_clickhouse.rs +++ b/tests/sqlparser_clickhouse.rs @@ -220,6 +220,14 @@ fn parse_delimited_identifiers() { //TODO verified_stmt(r#"UPDATE foo SET "bar" = 5"#); } +#[test] +fn parse_non_reserved_keyword_table_names() { + for table_name in ["into", "limit", "having", "where", "on"] { + clickhouse().verified_stmt(&format!("SELECT * FROM {table_name}")); + clickhouse().verified_stmt(&format!("SELECT * FROM t JOIN {table_name} ON true")); + } +} + #[test] fn parse_create_table() { clickhouse().verified_stmt(r#"CREATE TABLE "x" ("a" "int") ENGINE = MergeTree ORDER BY ("x")"#); @@ -2418,11 +2426,3 @@ fn parse_create_table_engine_name_quoting() { clickhouse_and_generic().verified_stmt("CREATE TABLE t (a INT) ENGINE = ``"); clickhouse().verified_stmt("CREATE TABLE t (a INT) ENGINE = MergeTree"); } - -#[test] -fn parse_non_reserved_keyword_table_names() { - for table_name in ["into", "limit", "having", "where", "on"] { - clickhouse().verified_stmt(&format!("SELECT * FROM {table_name}")); - clickhouse().verified_stmt(&format!("SELECT * FROM t JOIN {table_name} ON true")); - } -} diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index e1df4a9154..a9af201055 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -20903,6 +20903,29 @@ fn parse_hex_string_literal_display_escaping() { all_dialects().one_statement_parses_to("SELECT x'''' N", "SELECT X'''' AS N"); } +#[test] +fn on_keyword_rejected_as_table_name() { + let dialects = all_dialects_where(|d| !d.is_table_factor(&Keyword::ON, &mut Parser::new(d))); + for sql in [ + "SELECT * FROM ON", + "SELECT * FROM t JOIN ON", + "SELECT * FROM t JOIN ON u ON TRUE", + ] { + assert_eq!( + ParserError::ParserError("Expected: table name, found: ON".to_string()), + dialects.parse_sql_statements(sql).unwrap_err() + ); + } +} + +#[test] +fn quoted_on_allowed_as_table_name() { + let dialects = all_dialects_where(|d| d.is_delimited_identifier_start('"')); + dialects.verified_stmt(r#"SELECT * FROM "ON""#); + let dialects = all_dialects_where(|d| d.is_delimited_identifier_start('`')); + dialects.verified_stmt("SELECT * FROM `ON`"); +} + #[test] fn parse_stage_table_factor() { let supported = all_dialects_where(|d| d.supports_stages()); @@ -21319,21 +21342,6 @@ fn unary_prefix_operator_renders_apart_from_cast_operand() { } } -#[test] -fn on_keyword_rejected_as_table_name() { - let dialects = all_dialects_where(|d| !d.is_table_factor(&Keyword::ON, &mut Parser::new(d))); - for sql in [ - "SELECT * FROM ON", - "SELECT * FROM t JOIN ON", - "SELECT * FROM t JOIN ON u ON TRUE", - ] { - assert_eq!( - ParserError::ParserError("Expected: table name, found: ON".to_string()), - dialects.parse_sql_statements(sql).unwrap_err() - ); - } -} - #[test] fn parse_string_literal_alias() { for (sql, canonical, table, explicit, error) in [ @@ -21521,11 +21529,3 @@ fn parse_qualified_literal_prefix_requires_adjacent_period() { dialects.tokenizes_to(sql, vec![token]); } } - -#[test] -fn quoted_on_allowed_as_table_name() { - let dialects = all_dialects_where(|d| d.is_delimited_identifier_start('"')); - dialects.verified_stmt(r#"SELECT * FROM "ON""#); - let dialects = all_dialects_where(|d| d.is_delimited_identifier_start('`')); - dialects.verified_stmt("SELECT * FROM `ON`"); -} diff --git a/tests/sqlparser_databricks.rs b/tests/sqlparser_databricks.rs index e126c83c9b..9c75faf492 100644 --- a/tests/sqlparser_databricks.rs +++ b/tests/sqlparser_databricks.rs @@ -55,6 +55,12 @@ fn test_databricks_identifiers() { ); } +#[test] +fn parse_non_reserved_keyword_table_names() { + databricks().verified_stmt("SELECT * FROM limit"); + databricks().verified_stmt("SELECT * FROM t JOIN limit ON true"); +} + #[test] fn test_databricks_exists() { // exists is a function in databricks @@ -784,9 +790,3 @@ fn parse_databricks_collated_data_types() { )) ); } - -#[test] -fn parse_non_reserved_keyword_table_names() { - databricks().verified_stmt("SELECT * FROM limit"); - databricks().verified_stmt("SELECT * FROM t JOIN limit ON true"); -} diff --git a/tests/sqlparser_hive.rs b/tests/sqlparser_hive.rs index a9ed84cc67..ab4d4f141a 100644 --- a/tests/sqlparser_hive.rs +++ b/tests/sqlparser_hive.rs @@ -233,6 +233,12 @@ fn test_identifier() { hive().verified_stmt(between); } +#[test] +fn parse_non_reserved_keyword_table_names() { + hive().verified_stmt("SELECT * FROM limit"); + hive().verified_stmt("SELECT * FROM t JOIN limit ON true"); +} + #[test] fn test_alter_partition() { let alter = "ALTER TABLE db.table PARTITION (a = 2) RENAME TO PARTITION (a = 1)"; @@ -576,9 +582,3 @@ fn hive() -> TestedDialects { fn hive_and_generic() -> TestedDialects { TestedDialects::new(vec![Box::new(HiveDialect {}), Box::new(GenericDialect {})]) } - -#[test] -fn parse_non_reserved_keyword_table_names() { - hive().verified_stmt("SELECT * FROM limit"); - hive().verified_stmt("SELECT * FROM t JOIN limit ON true"); -} diff --git a/tests/sqlparser_mssql.rs b/tests/sqlparser_mssql.rs index 2fd8e13975..62f3a53efd 100644 --- a/tests/sqlparser_mssql.rs +++ b/tests/sqlparser_mssql.rs @@ -2670,6 +2670,12 @@ fn test_sql_keywords_as_table_aliases() { } } +#[test] +fn parse_non_reserved_keyword_table_names() { + ms().verified_stmt("SELECT * FROM limit"); + ms().verified_stmt("SELECT * FROM t JOIN limit ON true"); +} + #[test] fn test_sql_keywords_as_column_aliases() { // Some keywords that should not be parsed as an alias implicitly or explicitly @@ -3013,9 +3019,3 @@ fn parse_mssql_declare_comma_list_display() { ms().verified_stmt("DECLARE @a INT = 1, @b TEXT = 'x'"); ms().verified_stmt("DECLARE @foo CURSOR, @bar INT"); } - -#[test] -fn parse_non_reserved_keyword_table_names() { - ms().verified_stmt("SELECT * FROM limit"); - ms().verified_stmt("SELECT * FROM t JOIN limit ON true"); -} From fd43545463721c7b22c000799fbde624053258b9 Mon Sep 17 00:00:00 2001 From: LucaCappelletti94 Date: Sat, 24 Oct 2026 13:32:50 +0200 Subject: [PATCH 5/6] Permit Databricks keyword table names --- src/dialect/databricks.rs | 9 ++++----- tests/sqlparser_common.rs | 4 ++-- tests/sqlparser_databricks.rs | 11 +++++++++-- 3 files changed, 15 insertions(+), 9 deletions(-) diff --git a/src/dialect/databricks.rs b/src/dialect/databricks.rs index f973845f39..63dff58747 100644 --- a/src/dialect/databricks.rs +++ b/src/dialect/databricks.rs @@ -16,7 +16,7 @@ // under the License. use crate::dialect::Dialect; -use crate::keywords::{Keyword, RESERVED_FOR_TABLE_FACTOR}; +use crate::keywords::Keyword; use crate::parser::Parser; /// A [`Dialect`] for [Databricks SQL](https://www.databricks.com/) @@ -59,10 +59,9 @@ impl Dialect for DatabricksDialect { matches!(ch, 'a'..='z' | 'A'..='Z' | '0'..='9' | '_') } - /// LIMIT is not a reserved word in Databricks SQL. - /// See - fn is_table_factor(&self, kw: &Keyword, _parser: &mut Parser) -> bool { - kw == &Keyword::LIMIT || !RESERVED_FOR_TABLE_FACTOR.contains(kw) + /// Databricks permits [keywords](https://docs.databricks.com/aws/en/sql/language-manual/sql-ref-reserved-words) as table names. + fn is_table_factor(&self, _kw: &Keyword, _parser: &mut Parser) -> bool { + true } fn supports_numeric_prefix(&self) -> bool { diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index a9af201055..f71869e820 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -18414,8 +18414,8 @@ fn parse_pipeline_operator_negative_tests() { ), // Test that JOIN without table name fails ( - "SELECT * FROM users |> JOIN ON users.id = orders.user_id", - "Expected: table name, found: ON", + "SELECT * FROM users |> JOIN", + "Expected: identifier, found: EOF", ), // Test that CROSS JOIN with ON condition fails ( diff --git a/tests/sqlparser_databricks.rs b/tests/sqlparser_databricks.rs index 9c75faf492..ce26acc621 100644 --- a/tests/sqlparser_databricks.rs +++ b/tests/sqlparser_databricks.rs @@ -57,8 +57,15 @@ fn test_databricks_identifiers() { #[test] fn parse_non_reserved_keyword_table_names() { - databricks().verified_stmt("SELECT * FROM limit"); - databricks().verified_stmt("SELECT * FROM t JOIN limit ON true"); + for name in ["limit", "where", "on", "into", "having"] { + let sql = format!("SELECT * FROM {name}"); + let select = databricks().verified_only_select(&sql); + assert_eq!(select.from[0].relation, table(name)); + + let sql = format!("SELECT * FROM t JOIN {name} ON true"); + let select = databricks().verified_only_select(&sql); + assert_eq!(select.from[0].joins[0].relation, table(name)); + } } #[test] From badd49b66d3ebf7713c37c9536c346865eed3e67 Mon Sep 17 00:00:00 2001 From: LucaCappelletti94 Date: Sat, 24 Oct 2026 13:46:39 +0200 Subject: [PATCH 6/6] Add fuzz seeds for reject on as table name --- fuzz/fuzz_seeds/clickhouse_2633_keyword_table_names.sql | 2 ++ fuzz/fuzz_seeds/generic_2633_quoted_keyword_table_names.sql | 2 ++ 2 files changed, 4 insertions(+) create mode 100644 fuzz/fuzz_seeds/clickhouse_2633_keyword_table_names.sql create mode 100644 fuzz/fuzz_seeds/generic_2633_quoted_keyword_table_names.sql diff --git a/fuzz/fuzz_seeds/clickhouse_2633_keyword_table_names.sql b/fuzz/fuzz_seeds/clickhouse_2633_keyword_table_names.sql new file mode 100644 index 0000000000..653dfbe79e --- /dev/null +++ b/fuzz/fuzz_seeds/clickhouse_2633_keyword_table_names.sql @@ -0,0 +1,2 @@ +SELECT * FROM ON; +SELECT * FROM t JOIN limit ON true; diff --git a/fuzz/fuzz_seeds/generic_2633_quoted_keyword_table_names.sql b/fuzz/fuzz_seeds/generic_2633_quoted_keyword_table_names.sql new file mode 100644 index 0000000000..4bbe5464e2 --- /dev/null +++ b/fuzz/fuzz_seeds/generic_2633_quoted_keyword_table_names.sql @@ -0,0 +1,2 @@ +SELECT * FROM "ON"; +SELECT * FROM `ON`;