diff --git a/src/dialect/keywords.rs b/src/dialect/keywords.rs index 42e4c6a5..7c7eb494 100644 --- a/src/dialect/keywords.rs +++ b/src/dialect/keywords.rs @@ -1,14 +1,16 @@ -///! This module defines a list of constants for every keyword that +///! This module defines +/// 1) a list of constants for every keyword that /// can appear in SQLWord::keyword: /// pub const KEYWORD = "KEYWORD" -/// and an `ALL_KEYWORDS` array with every keyword in it. +/// 2) an `ALL_KEYWORDS` array with every keyword in it +/// This is not a list of *reserved* keywords: some of these can be +/// parsed as identifiers if the parser decides so. This means that +/// new keywords can be added here without affecting the parse result. /// -/// This is not a list of *reserved* keywords: some of these can be -/// parsed as identifiers if the parser decides so. This means that -/// new keywords can be added here without affecting the parse result. -/// -/// As a matter of fact, most of these keywords are not used at all -/// and could be removed. +/// As a matter of fact, most of these keywords are not used at all +/// and could be removed. +/// 3) a `RESERVED_FOR_TABLE_ALIAS` array with keywords reserved in a +/// "table alias" context. macro_rules! keyword { ($($ident:ident),*) => { @@ -707,3 +709,12 @@ pub const ALL_KEYWORDS: &'static [&'static str] = &[ ZONE, END_EXEC, ]; + +/// These keywords can't be used as a table alias, so that `FROM table_name alias` +/// can be parsed unambiguously without looking ahead. +pub const RESERVED_FOR_TABLE_ALIAS: &'static [&'static str] = &[ + WHERE, GROUP, ON, // keyword is 'reserved' in most dialects + JOIN, INNER, CROSS, FULL, LEFT, RIGHT, // not reserved in Oracle + NATURAL, USING, // not reserved in Oracle & MSSQL + // UNION, EXCEPT, INTERSECT, ORDER // TODO add these with tests. +]; diff --git a/src/sqlparser.rs b/src/sqlparser.rs index 3d0451b0..33b950de 100644 --- a/src/sqlparser.rs +++ b/src/sqlparser.rs @@ -14,6 +14,7 @@ //! SQL Parser +use super::dialect::keywords; use super::dialect::Dialect; use super::sqlast::*; use super::sqltokenizer::*; @@ -950,13 +951,18 @@ impl Parser { /// `SELECT ... FROM t1 foo, t2 bar`, `SELECT ... FROM (...) AS bar` pub fn parse_optional_alias( &mut self, + reserved_kwds: &[&str], ) -> Result, ParserError> { let after_as = self.parse_keyword("AS"); let maybe_alias = self.next_token(); match maybe_alias { // Accept any identifier after `AS` (though many dialects have restrictions on - // keywords that may appear here). - Some(Token::SQLWord(ref w)) if after_as => + // keywords that may appear here). If there's no `AS`: don't parse keywords, + // which may start a construct allowed in this position, to be parsed as aliases. + // (For example, in `FROM t1 JOIN` the `JOIN` will always be parsed as a keyword, + // not an alias.) + Some(Token::SQLWord(ref w)) + if after_as || !reserved_kwds.contains(&w.keyword.as_str()) => { // have to clone here until #![feature(bind_by_move_pattern_guards)] is enabled by default Ok(Some(w.value.clone())) @@ -1157,7 +1163,7 @@ impl Parser { } else { self.parse_compound_identifier(&Token::Period)? }; - let alias = self.parse_optional_alias()?; + let alias = self.parse_optional_alias(keywords::RESERVED_FOR_TABLE_ALIAS)?; Ok(ASTNode::TableFactor { relation: Box::new(relation), alias, diff --git a/tests/sqlparser_generic.rs b/tests/sqlparser_generic.rs index d850089a..ec4e26f5 100644 --- a/tests/sqlparser_generic.rs +++ b/tests/sqlparser_generic.rs @@ -601,6 +601,10 @@ fn parse_joins_on() { JoinOperator::Inner )] ); + parses_to( + "SELECT * FROM t1 JOIN t2 foo ON c1 = c2", + "SELECT * FROM t1 JOIN t2 AS foo ON c1 = c2", + ); // Test parsing of different join operators assert_eq!( joins_from(verified("SELECT * FROM t1 JOIN t2 ON c1 = c2")), @@ -644,6 +648,10 @@ fn parse_joins_using() { JoinOperator::Inner )] ); + parses_to( + "SELECT * FROM t1 JOIN t2 foo USING(c1)", + "SELECT * FROM t1 JOIN t2 AS foo USING(c1)", + ); // Test parsing of different join operators assert_eq!( joins_from(verified("SELECT * FROM t1 JOIN t2 USING(c1)")),