Skip to content

Commit dfa12e9

Browse files
Reject unquoted reserved keywords as table names
1 parent dacdbc9 commit dfa12e9

13 files changed

Lines changed: 113 additions & 7 deletions

‎src/dialect/clickhouse.rs‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
// under the License.
1717

1818
use crate::dialect::Dialect;
19+
use crate::keywords::Keyword;
20+
use crate::parser::Parser;
1921

2022
/// A [`Dialect`] for [ClickHouse](https://clickhouse.com/).
2123
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
@@ -32,6 +34,12 @@ impl Dialect for ClickHouseDialect {
3234
self.is_identifier_start(ch) || ch.is_ascii_digit()
3335
}
3436

37+
/// ClickHouse keywords are not reserved, so any of them can be a table name.
38+
/// See <https://clickhouse.com/docs/en/sql-reference/syntax>
39+
fn is_table_factor(&self, _kw: &Keyword, _parser: &mut Parser) -> bool {
40+
true
41+
}
42+
3543
/// ClickHouse accepts both backticks and double quotes, but its own
3644
/// formatter emits backticks by default.
3745
/// See <https://clickhouse.com/docs/en/sql-reference/syntax#identifiers>

‎src/dialect/databricks.rs‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
// under the License.
1717

1818
use crate::dialect::Dialect;
19+
use crate::keywords::{Keyword, RESERVED_FOR_TABLE_FACTOR};
20+
use crate::parser::Parser;
1921

2022
/// A [`Dialect`] for [Databricks SQL](https://www.databricks.com/)
2123
///
@@ -44,6 +46,12 @@ impl Dialect for DatabricksDialect {
4446
matches!(ch, 'a'..='z' | 'A'..='Z' | '0'..='9' | '_')
4547
}
4648

49+
/// LIMIT is not a reserved word in Databricks SQL.
50+
/// See <https://docs.databricks.com/aws/en/sql/language-manual/sql-ref-reserved-words>
51+
fn is_table_factor(&self, kw: &Keyword, _parser: &mut Parser) -> bool {
52+
kw == &Keyword::LIMIT || !RESERVED_FOR_TABLE_FACTOR.contains(kw)
53+
}
54+
4755
fn supports_numeric_prefix(&self) -> bool {
4856
true
4957
}

‎src/dialect/hive.rs‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
// under the License.
1717

1818
use crate::dialect::Dialect;
19+
use crate::keywords::{Keyword, RESERVED_FOR_TABLE_FACTOR};
20+
use crate::parser::Parser;
1921

2022
/// A [`Dialect`] for [Hive](https://hive.apache.org/).
2123
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
@@ -46,6 +48,12 @@ impl Dialect for HiveDialect {
4648
|| ch == '}'
4749
}
4850

51+
/// LIMIT is not reserved in Hive; identifiers may be nonReserved keywords.
52+
/// See <https://github.com/apache/hive/blob/4e9caba7c02a947a39c60de427acb7189c8c1fe8/parser/src/java/org/apache/hadoop/hive/ql/parse/IdentifiersParser.g#L945>
53+
fn is_table_factor(&self, kw: &Keyword, _parser: &mut Parser) -> bool {
54+
kw == &Keyword::LIMIT || !RESERVED_FOR_TABLE_FACTOR.contains(kw)
55+
}
56+
4957
fn supports_filter_during_aggregation(&self) -> bool {
5058
true
5159
}

‎src/dialect/mssql.rs‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ use crate::ast::{
2121
GranteesType, IfStatement, Statement,
2222
};
2323
use crate::dialect::Dialect;
24-
use crate::keywords::Keyword;
24+
use crate::keywords::{Keyword, RESERVED_FOR_TABLE_FACTOR};
2525
use crate::parser::{Parser, ParserError};
2626
use crate::tokenizer::Token;
2727
#[cfg(not(feature = "std"))]
@@ -167,6 +167,12 @@ impl Dialect for MsSqlDialect {
167167
}
168168
}
169169

170+
/// LIMIT is not a reserved word in Transact-SQL.
171+
/// See <https://learn.microsoft.com/en-us/sql/t-sql/language-elements/reserved-keywords-transact-sql>
172+
fn is_table_factor(&self, kw: &Keyword, _parser: &mut Parser) -> bool {
173+
kw == &Keyword::LIMIT || !RESERVED_FOR_TABLE_FACTOR.contains(kw)
174+
}
175+
170176
fn is_table_factor_alias(&self, explicit: bool, kw: &Keyword, parser: &mut Parser) -> bool {
171177
match kw {
172178
// List of keywords that cannot be used as table aliases in MSSQL

‎src/dialect/oracle.rs‎

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,10 @@ use crate::{
2222
tokenizer::Token,
2323
};
2424

25-
use super::{keywords::Keyword, Dialect, Precedence};
25+
use super::{
26+
keywords::{Keyword, RESERVED_FOR_TABLE_FACTOR},
27+
Dialect, Precedence,
28+
};
2629

2730
const RESERVED_KEYWORDS_FOR_SELECT_ITEM_OPERATOR: [Keyword; 1] = [Keyword::CONNECT_BY_ROOT];
2831

@@ -49,6 +52,12 @@ impl Dialect for OracleDialect {
4952
ch.is_alphanumeric() || ch == '_' || ch == '$' || ch == '#' || ch == '@'
5053
}
5154

55+
/// LIMIT is not a reserved word in Oracle SQL.
56+
/// See <https://docs.oracle.com/en/database/oracle/oracle-database/19/sqlrf/Oracle-SQL-Reserved-Words.html>
57+
fn is_table_factor(&self, kw: &Keyword, _parser: &mut Parser) -> bool {
58+
kw == &Keyword::LIMIT || !RESERVED_FOR_TABLE_FACTOR.contains(kw)
59+
}
60+
5261
fn supports_outer_join_operator(&self) -> bool {
5362
true
5463
}

‎src/keywords.rs‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1300,14 +1300,16 @@ pub const RESERVED_FOR_COLUMN_ALIAS: &[Keyword] = &[
13001300
Keyword::END,
13011301
];
13021302

1303-
/// Global list of reserved keywords allowed after FROM.
1304-
/// Parser should call Dialect::get_reserved_keyword_after_from
1305-
/// to allow for each dialect to customize the list.
1303+
/// Global list of reserved keywords that cannot be parsed as a table
1304+
/// factor name without special handling like quoting. Parser should
1305+
/// call `Dialect::is_table_factor` to allow for each dialect to
1306+
/// customize the list.
13061307
pub const RESERVED_FOR_TABLE_FACTOR: &[Keyword] = &[
13071308
Keyword::INTO,
13081309
Keyword::LIMIT,
13091310
Keyword::HAVING,
13101311
Keyword::WHERE,
1312+
Keyword::ON,
13111313
];
13121314

13131315
/// Global list of reserved keywords that cannot be parsed as identifiers

‎src/parser/mod.rs‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16934,6 +16934,16 @@ impl<'a> Parser<'a> {
1693416934
// Stage reference: @mystage or @namespace.stage (e.g. Snowflake)
1693516935
self.parse_snowflake_stage_table_factor()
1693616936
} else {
16937+
// Unquoted reserved keywords, e.g. `ON`, are not table names.
16938+
let peek_keyword = if let Token::Word(w) = &self.peek_token_ref().token {
16939+
w.keyword
16940+
} else {
16941+
Keyword::NoKeyword
16942+
};
16943+
if !self.dialect.is_table_factor(&peek_keyword, self) {
16944+
return self.expected_ref("table name", self.peek_token_ref());
16945+
}
16946+
1693716947
let name = self.parse_object_name(true)?;
1693816948

1693916949
let json_path = match &self.peek_token_ref().token {

‎tests/sqlparser_clickhouse.rs‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2213,3 +2213,11 @@ fn clickhouse_and_generic() -> TestedDialects {
22132213
Box::new(GenericDialect {}),
22142214
])
22152215
}
2216+
2217+
#[test]
2218+
fn parse_non_reserved_keyword_table_names() {
2219+
for table_name in ["into", "limit", "having", "where", "on"] {
2220+
clickhouse().verified_stmt(&format!("SELECT * FROM {table_name}"));
2221+
clickhouse().verified_stmt(&format!("SELECT * FROM t JOIN {table_name} ON true"));
2222+
}
2223+
}

‎tests/sqlparser_common.rs‎

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1207,10 +1207,10 @@ fn parse_select_wildcard() {
12071207

12081208
#[test]
12091209
fn parse_count_wildcard() {
1210-
verified_only_select("SELECT COUNT(*) FROM Order WHERE id = 10");
1210+
verified_only_select("SELECT COUNT(*) FROM orders WHERE id = 10");
12111211

12121212
verified_only_select(
1213-
"SELECT COUNT(Employee.*) FROM Order JOIN Employee ON Order.employee = Employee.id",
1213+
"SELECT COUNT(Employee.*) FROM orders JOIN Employee ON orders.employee = Employee.id",
12141214
);
12151215
}
12161216

@@ -20511,3 +20511,26 @@ fn parse_position_in_connect_by_list_after_state_change() {
2051120511
assert_eq!(positions, 1, "{dialect:?}");
2051220512
}
2051320513
}
20514+
20515+
#[test]
20516+
fn on_keyword_rejected_as_table_name() {
20517+
let dialects = all_dialects_where(|d| !d.is_table_factor(&Keyword::ON, &mut Parser::new(d)));
20518+
for sql in [
20519+
"SELECT * FROM ON",
20520+
"SELECT * FROM t JOIN ON",
20521+
"SELECT * FROM t JOIN ON u ON TRUE",
20522+
] {
20523+
assert_eq!(
20524+
ParserError::ParserError("Expected: table name, found: ON".to_string()),
20525+
dialects.parse_sql_statements(sql).unwrap_err()
20526+
);
20527+
}
20528+
}
20529+
20530+
#[test]
20531+
fn quoted_on_allowed_as_table_name() {
20532+
let dialects = all_dialects_where(|d| d.is_delimited_identifier_start('"'));
20533+
dialects.verified_stmt(r#"SELECT * FROM "ON""#);
20534+
let dialects = all_dialects_where(|d| d.is_delimited_identifier_start('`'));
20535+
dialects.verified_stmt("SELECT * FROM `ON`");
20536+
}

‎tests/sqlparser_databricks.rs‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -769,3 +769,9 @@ fn parse_databricks_collated_data_types() {
769769
.parse_sql_statements("CREATE TABLE t (c ARRAY<STRING COLLATE UTF8_LCASE>)")
770770
.is_err());
771771
}
772+
773+
#[test]
774+
fn parse_non_reserved_keyword_table_names() {
775+
databricks().verified_stmt("SELECT * FROM limit");
776+
databricks().verified_stmt("SELECT * FROM t JOIN limit ON true");
777+
}

0 commit comments

Comments
 (0)