Skip to content

Commit 76ec175

Browse files
committed
Support table aliases without AS (7/8)
...as in `FROM foo bar WHERE bar.x > 1`. To avoid ambiguity as to whether a token is an alias or a keyword, we maintain a blacklist of keywords, that can follow a "table factor", to prevent parsing them as an alias. This "context-specific reserved keyword" approach lets us accept more SQL that's valid in some dialects, than a list of globally reserved keywords. Also some dialects (e.g. Oracle) apparently don't reserve some keywords (like JOIN), while presumably they won't accept them as an alias (`FROM foo JOIN` meaning `FROM foo AS JOIN`).
1 parent 536fa6e commit 76ec175

File tree

3 files changed

+36
-11
lines changed

3 files changed

+36
-11
lines changed

src/dialect/keywords.rs

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
1-
///! This module defines a list of constants for every keyword that
1+
///! This module defines
2+
/// 1) a list of constants for every keyword that
23
/// can appear in SQLWord::keyword:
34
/// pub const KEYWORD = "KEYWORD"
4-
/// and an `ALL_KEYWORDS` array with every keyword in it.
5+
/// 2) an `ALL_KEYWORDS` array with every keyword in it
6+
/// This is not a list of *reserved* keywords: some of these can be
7+
/// parsed as identifiers if the parser decides so. This means that
8+
/// new keywords can be added here without affecting the parse result.
59
///
6-
/// This is not a list of *reserved* keywords: some of these can be
7-
/// parsed as identifiers if the parser decides so. This means that
8-
/// new keywords can be added here without affecting the parse result.
9-
///
10-
/// As a matter of fact, most of these keywords are not used at all
11-
/// and could be removed.
10+
/// As a matter of fact, most of these keywords are not used at all
11+
/// and could be removed.
12+
/// 3) a `RESERVED_FOR_TABLE_ALIAS` array with keywords reserved in a
13+
/// "table alias" context.
1214
1315
macro_rules! keyword {
1416
($($ident:ident),*) => {
@@ -707,3 +709,12 @@ pub const ALL_KEYWORDS: &'static [&'static str] = &[
707709
ZONE,
708710
END_EXEC,
709711
];
712+
713+
/// These keywords can't be used as a table alias, so that `FROM table_name alias`
714+
/// can be parsed unambiguously without looking ahead.
715+
pub const RESERVED_FOR_TABLE_ALIAS: &'static [&'static str] = &[
716+
WHERE, GROUP, ON, // keyword is 'reserved' in most dialects
717+
JOIN, INNER, CROSS, FULL, LEFT, RIGHT, // not reserved in Oracle
718+
NATURAL, USING, // not reserved in Oracle & MSSQL
719+
// UNION, EXCEPT, INTERSECT, ORDER // TODO add these with tests.
720+
];

src/sqlparser.rs

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
//! SQL Parser
1616
17+
use super::dialect::keywords;
1718
use super::dialect::Dialect;
1819
use super::sqlast::*;
1920
use super::sqltokenizer::*;
@@ -950,13 +951,18 @@ impl Parser {
950951
/// `SELECT ... FROM t1 foo, t2 bar`, `SELECT ... FROM (...) AS bar`
951952
pub fn parse_optional_alias(
952953
&mut self,
954+
reserved_kwds: &[&str],
953955
) -> Result<Option<SQLIdent>, ParserError> {
954956
let after_as = self.parse_keyword("AS");
955957
let maybe_alias = self.next_token();
956958
match maybe_alias {
957959
// Accept any identifier after `AS` (though many dialects have restrictions on
958-
// keywords that may appear here).
959-
Some(Token::SQLWord(ref w)) if after_as =>
960+
// keywords that may appear here). If there's no `AS`: don't parse keywords,
961+
// which may start a construct allowed in this position, to be parsed as aliases.
962+
// (For example, in `FROM t1 JOIN` the `JOIN` will always be parsed as a keyword,
963+
// not an alias.)
964+
Some(Token::SQLWord(ref w))
965+
if after_as || !reserved_kwds.contains(&w.keyword.as_str()) =>
960966
{
961967
// have to clone here until #![feature(bind_by_move_pattern_guards)] is enabled by default
962968
Ok(Some(w.value.clone()))
@@ -1157,7 +1163,7 @@ impl Parser {
11571163
} else {
11581164
self.parse_compound_identifier(&Token::Period)?
11591165
};
1160-
let alias = self.parse_optional_alias()?;
1166+
let alias = self.parse_optional_alias(keywords::RESERVED_FOR_TABLE_ALIAS)?;
11611167
Ok(ASTNode::TableFactor {
11621168
relation: Box::new(relation),
11631169
alias,

tests/sqlparser_generic.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -601,6 +601,10 @@ fn parse_joins_on() {
601601
JoinOperator::Inner
602602
)]
603603
);
604+
parses_to(
605+
"SELECT * FROM t1 JOIN t2 foo ON c1 = c2",
606+
"SELECT * FROM t1 JOIN t2 AS foo ON c1 = c2",
607+
);
604608
// Test parsing of different join operators
605609
assert_eq!(
606610
joins_from(verified("SELECT * FROM t1 JOIN t2 ON c1 = c2")),
@@ -644,6 +648,10 @@ fn parse_joins_using() {
644648
JoinOperator::Inner
645649
)]
646650
);
651+
parses_to(
652+
"SELECT * FROM t1 JOIN t2 foo USING(c1)",
653+
"SELECT * FROM t1 JOIN t2 AS foo USING(c1)",
654+
);
647655
// Test parsing of different join operators
648656
assert_eq!(
649657
joins_from(verified("SELECT * FROM t1 JOIN t2 USING(c1)")),

0 commit comments

Comments
 (0)