[mssql] Parse CROSS/OUTER APPLY

T-SQL (and Oracle) support non-standard syntax, which is similar in
functionality to LATERAL joins in ANSI and PostgreSQL
<https://blog.jooq.org/tag/lateral-derived-table/>: it allows to use
the columns from the tables defined to the left of `APPLY` in the
"derived tables" (subqueries) to the right of `APPLY`. Unlike ANSI
LATERAL (but like Postgres' implementation), APPLY is also used with
table-valued function calls.

Despite them being similar, we represent "APPLY" joins with
`JoinOperator`s of its own (`CrossApply` and `OuterApply`). Doing
otherwise seemed like it would cause unnecessary confusion, as those
interested in dialect-specific parsing would probably not expect APPLY
being parsed as LATERAL, and those wanting to forbid non-standard SQL
would not be helped by this either.

This also renames existing JoinOperator::Cross -> CrossJoin to avoid
confusion with CrossApply.
This commit is contained in:
Nickolay Ponomarev 2019-06-19 02:26:51 +03:00
parent 0f6bf15258
commit 4294581ded
5 changed files with 44 additions and 10 deletions

View file

@ -58,6 +58,7 @@ define_keywords!(
ALTER,
AND,
ANY,
APPLY,
ARE,
ARRAY,
ARRAY_AGG,
@ -423,10 +424,7 @@ pub const RESERVED_FOR_TABLE_ALIAS: &[&str] = &[
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,
// 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.
// for MSSQL-specific OUTER APPLY (seems reserved in most dialects)
OUTER,
];

View file

@ -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)]

View file

@ -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: JoinOperator::Cross,
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::OuterApply,
}
} else {
let natural = self.parse_keyword("NATURAL");

View file

@ -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()
);
}

View file

@ -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 {})],