diff --git a/src/dialect/keywords.rs b/src/dialect/keywords.rs index 4a50c7fce..4fc103f37 100644 --- a/src/dialect/keywords.rs +++ b/src/dialect/keywords.rs @@ -58,6 +58,7 @@ define_keywords!( ALTER, AND, ANY, + APPLY, ARE, ARRAY, ARRAY_AGG, @@ -422,11 +423,8 @@ pub const RESERVED_FOR_TABLE_ALIAS: &[&str] = &[ // Reserved as both a table and a column alias: WITH, SELECT, WHERE, GROUP, HAVING, ORDER, LIMIT, OFFSET, FETCH, UNION, EXCEPT, INTERSECT, // Reserved only as a table alias in the `FROM`/`JOIN` clauses: - ON, JOIN, INNER, CROSS, FULL, LEFT, RIGHT, NATURAL, USING, LIMIT, OFFSET, FETCH, - // Reserved not because of ambiguity, but so that parsing `SELECT * FROM a - // OUTER JOIN b` causes a syntax error, rather than silently parsing to an - // inner join where table `a` is aliased as `OUTER`, which is certainly not - // what the user intended and also not valid according to the SQL standard. + ON, JOIN, INNER, CROSS, FULL, LEFT, RIGHT, NATURAL, USING, + // for MSSQL-specific OUTER APPLY (seems reserved in most dialects) OUTER, ]; diff --git a/src/sqlast/query.rs b/src/sqlast/query.rs index 4c23c0af1..1ea99c21f 100644 --- a/src/sqlast/query.rs +++ b/src/sqlast/query.rs @@ -327,7 +327,6 @@ impl ToString for Join { self.relation.to_string(), suffix(constraint) ), - JoinOperator::Cross => format!(" CROSS JOIN {}", self.relation.to_string()), JoinOperator::LeftOuter(constraint) => format!( " {}LEFT JOIN {}{}", prefix(constraint), @@ -346,6 +345,9 @@ impl ToString for Join { self.relation.to_string(), suffix(constraint) ), + JoinOperator::CrossJoin => format!(" CROSS JOIN {}", self.relation.to_string()), + JoinOperator::CrossApply => format!(" CROSS APPLY {}", self.relation.to_string()), + JoinOperator::OuterApply => format!(" OUTER APPLY {}", self.relation.to_string()), } } } @@ -356,7 +358,11 @@ pub enum JoinOperator { LeftOuter(JoinConstraint), RightOuter(JoinConstraint), FullOuter(JoinConstraint), - Cross, + CrossJoin, + /// CROSS APPLY (non-standard) + CrossApply, + /// OUTER APPLY (non-standard) + OuterApply, } #[derive(Debug, Clone, PartialEq, Hash)] diff --git a/src/sqlparser.rs b/src/sqlparser.rs index 7802e5b85..490e8e5c5 100644 --- a/src/sqlparser.rs +++ b/src/sqlparser.rs @@ -1618,10 +1618,24 @@ impl Parser { let mut joins = vec![]; loop { let join = if self.parse_keyword("CROSS") { - self.expect_keyword("JOIN")?; + let join_operator = if self.parse_keyword("JOIN") { + JoinOperator::CrossJoin + } else if self.parse_keyword("APPLY") { + // MSSQL extension, similar to CROSS JOIN LATERAL + JoinOperator::CrossApply + } else { + return self.expected("JOIN or APPLY after CROSS", self.peek_token()); + }; + Join { + relation: self.parse_table_factor()?, + join_operator, + } + } else if self.parse_keyword("OUTER") { + // MSSQL extension, similar to LEFT JOIN LATERAL .. ON 1=1 + self.expect_keyword("APPLY")?; Join { relation: self.parse_table_factor()?, - join_operator: JoinOperator::Cross, + join_operator: JoinOperator::OuterApply, } } else { let natural = self.parse_keyword("NATURAL"); diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 139c91e07..d1446abc0 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -1560,7 +1560,7 @@ fn parse_cross_join() { args: vec![], with_hints: vec![], }, - join_operator: JoinOperator::Cross + join_operator: JoinOperator::CrossJoin }, only(only(select.from).joins), ); @@ -1804,7 +1804,7 @@ fn parse_join_syntax_variants() { let res = parse_sql_statements("SELECT * FROM a OUTER JOIN b ON 1"); assert_eq!( - ParserError::ParserError("Expected LEFT, RIGHT, or FULL, found: OUTER".to_string()), + ParserError::ParserError("Expected APPLY, found: JOIN".to_string()), res.unwrap_err() ); } diff --git a/tests/sqlparser_mssql.rs b/tests/sqlparser_mssql.rs index 2241a226d..7c83fdae7 100644 --- a/tests/sqlparser_mssql.rs +++ b/tests/sqlparser_mssql.rs @@ -52,6 +52,22 @@ fn parse_mssql_delimited_identifiers() { ); } +#[test] +fn parse_mssql_apply_join() { + let _ = ms_and_generic().verified_only_select( + "SELECT * FROM sys.dm_exec_query_stats AS deqs \ + CROSS APPLY sys.dm_exec_query_plan(deqs.plan_handle)", + ); + let _ = ms_and_generic().verified_only_select( + "SELECT * FROM sys.dm_exec_query_stats AS deqs \ + OUTER APPLY sys.dm_exec_query_plan(deqs.plan_handle)", + ); + let _ = ms_and_generic().verified_only_select( + "SELECT * FROM foo \ + OUTER APPLY (SELECT foo.x + 1) AS bar", + ); +} + fn ms() -> TestedDialects { TestedDialects { dialects: vec![Box::new(MsSqlDialect {})],