Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions src/dialect/clickhouse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand All @@ -32,6 +34,12 @@ impl Dialect for ClickHouseDialect {
self.is_identifier_start(ch) || ch.is_ascii_digit()
}

/// ClickHouse keywords are not reserved, so any of them can be a table name.
/// See <https://clickhouse.com/docs/en/sql-reference/syntax>
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 <https://clickhouse.com/docs/en/sql-reference/syntax#identifiers>
Expand Down
8 changes: 8 additions & 0 deletions src/dialect/databricks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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/)
///
Expand Down Expand Up @@ -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 <https://docs.databricks.com/aws/en/sql/language-manual/sql-ref-reserved-words>
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
}
Expand Down
8 changes: 8 additions & 0 deletions src/dialect/hive.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down Expand Up @@ -46,6 +48,12 @@ impl Dialect for HiveDialect {
|| ch == '}'
}

/// LIMIT is not reserved in Hive; identifiers may be nonReserved keywords.
/// See <https://github.com/apache/hive/blob/4e9caba7c02a947a39c60de427acb7189c8c1fe8/parser/src/java/org/apache/hadoop/hive/ql/parse/IdentifiersParser.g#L945>
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
}
Expand Down
8 changes: 7 additions & 1 deletion src/dialect/mssql.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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"))]
Expand Down Expand Up @@ -167,6 +167,12 @@ impl Dialect for MsSqlDialect {
}
}

/// LIMIT is not a reserved word in Transact-SQL.
/// See <https://learn.microsoft.com/en-us/sql/t-sql/language-elements/reserved-keywords-transact-sql>
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
Expand Down
11 changes: 10 additions & 1 deletion src/dialect/oracle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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];

Expand All @@ -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 <https://docs.oracle.com/en/database/oracle/oracle-database/19/sqlrf/Oracle-SQL-Reserved-Words.html>
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
}
Expand Down
8 changes: 5 additions & 3 deletions src/keywords.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
14 changes: 14 additions & 0 deletions src/parser/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
8 changes: 8 additions & 0 deletions tests/sqlparser_clickhouse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2384,3 +2384,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"));
}
}
29 changes: 26 additions & 3 deletions tests/sqlparser_common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1219,10 +1219,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");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This rename shows the change goes beyond ON: Snowflake's whole table-factor list now applies at the table position, and Generic rejects the five words too. Both match the docs, but they should be called out as breaking in the description/changelog.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately breaking changes are so common that it is almost a given that all versions are breaking versions. I hope we'll reach stability at some point, which is also why I am adding the fuzzers to accelerate that process as much as possible.


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",
);
}

Expand Down Expand Up @@ -17822,7 +17822,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
(
Expand Down Expand Up @@ -20700,10 +20700,33 @@ fn parse_cte_materialization() {
}
}

#[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());
dialects.one_statement_parses_to("SELECT 1 'x'", "SELECT 1 AS 'x'");
dialects.verified_stmt("SELECT 1 AS 'x'");
dialects.verified_stmt("SELECT a FROM t AS 'y'");
}

#[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`");
}
6 changes: 6 additions & 0 deletions tests/sqlparser_databricks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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");
}
6 changes: 6 additions & 0 deletions tests/sqlparser_hive.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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");
}
6 changes: 6 additions & 0 deletions tests/sqlparser_mssql.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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");
}
6 changes: 6 additions & 0 deletions tests/sqlparser_oracle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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");
}
Loading