diff --git a/src/dialect/clickhouse.rs b/src/dialect/clickhouse.rs index e94e5324b5..c859eadd93 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)] @@ -34,6 +36,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 ca8610dd6e..628522ca96 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/) /// @@ -44,6 +46,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 1a7d6de9e3..82f65fde3e 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)] @@ -50,6 +52,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 7e93822416..b67fbda764 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -1305,14 +1305,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 db54296fce..94bec022ec 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -17008,6 +17008,20 @@ 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. 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, + }; + 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()); + } + 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 bd216f940e..1ee60d52f9 100644 --- a/tests/sqlparser_clickhouse.rs +++ b/tests/sqlparser_clickhouse.rs @@ -2408,3 +2408,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 025c2cf207..a0d1c324b7 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -1218,10 +1218,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", ); } @@ -17870,7 +17870,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 ( @@ -20771,6 +20771,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() { let dialects = all_dialects_where(|d| d.supports_string_literal_alias()); @@ -20803,3 +20818,11 @@ fn display_nested_unary_minus_in_linear_time() { .expect("display should not take exponential time"); assert_eq!(printed, sql); } + +#[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 21e76e23b6..55765b47e1 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 e07d87007d..594c8fd8b5 100644 --- a/tests/sqlparser_mssql.rs +++ b/tests/sqlparser_mssql.rs @@ -3004,3 +3004,9 @@ fn parse_create_proc() { .expect_err("PROC should remain MSSQL-specific"); ms_and_generic().verified_stmt("SELECT proc FROM jobs"); } + +#[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"); +}