diff --git a/src/dialect/generic.rs b/src/dialect/generic.rs index fad750492c..326bb514ed 100644 --- a/src/dialect/generic.rs +++ b/src/dialect/generic.rs @@ -181,6 +181,10 @@ impl Dialect for GenericDialect { true } + fn supports_comma_join_constraint(&self) -> bool { + true + } + fn supports_comment_on(&self) -> bool { true } diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index 336cf829e4..b37fc3c86c 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -453,6 +453,14 @@ pub trait Dialect: Debug + Any { false } + /// Returns true if the dialect supports `ON`/`USING` after a comma-joined table, + /// e.g. `SELECT * FROM t, u ON t.a = u.a`. + /// + /// [SQLite](https://www.sqlite.org/syntax/join-clause.html) + fn supports_comma_join_constraint(&self) -> bool { + false + } + /// Returns true if the dialect supports CONNECT BY. fn supports_connect_by(&self) -> bool { false diff --git a/src/dialect/sqlite.rs b/src/dialect/sqlite.rs index ad700bb92f..16be3303bb 100644 --- a/src/dialect/sqlite.rs +++ b/src/dialect/sqlite.rs @@ -222,4 +222,8 @@ impl Dialect for SQLiteDialect { _ => !keywords::RESERVED_FOR_TABLE_ALIAS.contains(kw), } } + + fn supports_comma_join_constraint(&self) -> bool { + true + } } diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 20db2299b9..608e6c364f 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -5129,13 +5129,37 @@ impl<'a> Parser<'a> { /// Parse a list of [TableWithJoins] fn parse_table_with_joins(&mut self) -> Result, ParserError> { - let trailing_commas = self.dialect.supports_from_trailing_commas(); + let dialect = self.dialect; + let trailing_commas = dialect.supports_from_trailing_commas(); + let mut tables: Vec = vec![]; self.parse_comma_separated_with_trailing_commas( - Parser::parse_table_and_joins, + |parser| { + let table = parser.parse_table_and_joins()?; + match tables.last_mut() { + Some(previous) + if dialect.supports_comma_join_constraint() + && table.joins.is_empty() + && parser + .peek_one_of_keywords(&[Keyword::ON, Keyword::USING]) + .is_some() => + { + let constraint = parser.parse_join_constraint(false)?; + previous.joins.push(Join { + relation: table.relation, + global: false, + join_operator: JoinOperator::Join(constraint), + }); + previous.joins.extend(parser.parse_joins()?); + } + _ => tables.push(table), + } + Ok(()) + }, trailing_commas, - |kw, parser| !self.dialect.is_table_factor(kw, parser), - ) + |kw, parser| !dialect.is_table_factor(kw, parser), + )?; + Ok(tables) } /// Parse the comma of a comma-separated syntax element. diff --git a/tests/sqlparser_sqlite.rs b/tests/sqlparser_sqlite.rs index 1cc2dec8cf..07a279ae5d 100644 --- a/tests/sqlparser_sqlite.rs +++ b/tests/sqlparser_sqlite.rs @@ -1164,6 +1164,65 @@ fn parse_create_table_options_list() { } } +#[test] +fn parse_comma_join_constraint() { + let dialects = all_dialects_where(|d| d.supports_comma_join_constraint()); + for (sql, canonical) in [ + ( + "SELECT * FROM t, u ON t.a = u.a", + "SELECT * FROM t JOIN u ON t.a = u.a", + ), + ( + "SELECT * FROM t, u USING (a)", + "SELECT * FROM t JOIN u USING(a)", + ), + ( + "SELECT * FROM t, u ON t.a = u.a, v ON v.a = u.a", + "SELECT * FROM t JOIN u ON t.a = u.a JOIN v ON v.a = u.a", + ), + ( + "SELECT * FROM t, u ON t.a = u.a JOIN v ON v.a = u.a", + "SELECT * FROM t JOIN u ON t.a = u.a JOIN v ON v.a = u.a", + ), + ( + "UPDATE t SET a = 1 FROM u, v ON u.a = v.a", + "UPDATE t SET a = 1 FROM u JOIN v ON u.a = v.a", + ), + ] { + dialects.one_statement_parses_to(sql, canonical); + } + dialects.verified_stmt("SELECT * FROM t, u"); + + assert_eq!( + all_dialects_where(|d| !d.supports_comma_join_constraint()) + .parse_sql_statements("SELECT * FROM t, u ON t.a = u.a"), + Err(ParserError::ParserError( + "Expected: end of statement, found: ON".to_string() + )) + ); + + // Malformed inputs after a comma join constraint + for (sql, error) in [ + ( + "SELECT * FROM t, u ON", + "Expected: an expression, found: EOF", + ), + ( + "SELECT * FROM t, u USING a", + "Expected: a list of columns in parentheses, found: a", + ), + ( + "SELECT * FROM t, u ON t.a = u.a CROSS", + "Expected: JOIN or APPLY after CROSS, found: EOF", + ), + ] { + assert_eq!( + dialects.parse_sql_statements(sql), + Err(ParserError::ParserError(error.to_string())) + ); + } +} + #[test] fn parse_parenthesized_table_factor() { for (sql, canonical) in [