Skip to content

Commit 4294581

Browse files
committed
[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.
1 parent 0f6bf15 commit 4294581

File tree

5 files changed

+44
-10
lines changed

5 files changed

+44
-10
lines changed

src/dialect/keywords.rs

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ define_keywords!(
5858
ALTER,
5959
AND,
6060
ANY,
61+
APPLY,
6162
ARE,
6263
ARRAY,
6364
ARRAY_AGG,
@@ -423,10 +424,7 @@ pub const RESERVED_FOR_TABLE_ALIAS: &[&str] = &[
423424
WITH, SELECT, WHERE, GROUP, HAVING, ORDER, LIMIT, OFFSET, FETCH, UNION, EXCEPT, INTERSECT,
424425
// Reserved only as a table alias in the `FROM`/`JOIN` clauses:
425426
ON, JOIN, INNER, CROSS, FULL, LEFT, RIGHT, NATURAL, USING,
426-
// Reserved not because of ambiguity, but so that parsing `SELECT * FROM a
427-
// OUTER JOIN b` causes a syntax error, rather than silently parsing to an
428-
// inner join where table `a` is aliased as `OUTER`, which is certainly not
429-
// what the user intended and also not valid according to the SQL standard.
427+
// for MSSQL-specific OUTER APPLY (seems reserved in most dialects)
430428
OUTER,
431429
];
432430

src/sqlast/query.rs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -327,7 +327,6 @@ impl ToString for Join {
327327
self.relation.to_string(),
328328
suffix(constraint)
329329
),
330-
JoinOperator::Cross => format!(" CROSS JOIN {}", self.relation.to_string()),
331330
JoinOperator::LeftOuter(constraint) => format!(
332331
" {}LEFT JOIN {}{}",
333332
prefix(constraint),
@@ -346,6 +345,9 @@ impl ToString for Join {
346345
self.relation.to_string(),
347346
suffix(constraint)
348347
),
348+
JoinOperator::CrossJoin => format!(" CROSS JOIN {}", self.relation.to_string()),
349+
JoinOperator::CrossApply => format!(" CROSS APPLY {}", self.relation.to_string()),
350+
JoinOperator::OuterApply => format!(" OUTER APPLY {}", self.relation.to_string()),
349351
}
350352
}
351353
}
@@ -356,7 +358,11 @@ pub enum JoinOperator {
356358
LeftOuter(JoinConstraint),
357359
RightOuter(JoinConstraint),
358360
FullOuter(JoinConstraint),
359-
Cross,
361+
CrossJoin,
362+
/// CROSS APPLY (non-standard)
363+
CrossApply,
364+
/// OUTER APPLY (non-standard)
365+
OuterApply,
360366
}
361367

362368
#[derive(Debug, Clone, PartialEq, Hash)]

src/sqlparser.rs

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1618,10 +1618,24 @@ impl Parser {
16181618
let mut joins = vec![];
16191619
loop {
16201620
let join = if self.parse_keyword("CROSS") {
1621-
self.expect_keyword("JOIN")?;
1621+
let join_operator = if self.parse_keyword("JOIN") {
1622+
JoinOperator::CrossJoin
1623+
} else if self.parse_keyword("APPLY") {
1624+
// MSSQL extension, similar to CROSS JOIN LATERAL
1625+
JoinOperator::CrossApply
1626+
} else {
1627+
return self.expected("JOIN or APPLY after CROSS", self.peek_token());
1628+
};
1629+
Join {
1630+
relation: self.parse_table_factor()?,
1631+
join_operator,
1632+
}
1633+
} else if self.parse_keyword("OUTER") {
1634+
// MSSQL extension, similar to LEFT JOIN LATERAL .. ON 1=1
1635+
self.expect_keyword("APPLY")?;
16221636
Join {
16231637
relation: self.parse_table_factor()?,
1624-
join_operator: JoinOperator::Cross,
1638+
join_operator: JoinOperator::OuterApply,
16251639
}
16261640
} else {
16271641
let natural = self.parse_keyword("NATURAL");

tests/sqlparser_common.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1560,7 +1560,7 @@ fn parse_cross_join() {
15601560
args: vec![],
15611561
with_hints: vec![],
15621562
},
1563-
join_operator: JoinOperator::Cross
1563+
join_operator: JoinOperator::CrossJoin
15641564
},
15651565
only(only(select.from).joins),
15661566
);
@@ -1804,7 +1804,7 @@ fn parse_join_syntax_variants() {
18041804

18051805
let res = parse_sql_statements("SELECT * FROM a OUTER JOIN b ON 1");
18061806
assert_eq!(
1807-
ParserError::ParserError("Expected LEFT, RIGHT, or FULL, found: OUTER".to_string()),
1807+
ParserError::ParserError("Expected APPLY, found: JOIN".to_string()),
18081808
res.unwrap_err()
18091809
);
18101810
}

tests/sqlparser_mssql.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,22 @@ fn parse_mssql_delimited_identifiers() {
5252
);
5353
}
5454

55+
#[test]
56+
fn parse_mssql_apply_join() {
57+
let _ = ms_and_generic().verified_only_select(
58+
"SELECT * FROM sys.dm_exec_query_stats AS deqs \
59+
CROSS APPLY sys.dm_exec_query_plan(deqs.plan_handle)",
60+
);
61+
let _ = ms_and_generic().verified_only_select(
62+
"SELECT * FROM sys.dm_exec_query_stats AS deqs \
63+
OUTER APPLY sys.dm_exec_query_plan(deqs.plan_handle)",
64+
);
65+
let _ = ms_and_generic().verified_only_select(
66+
"SELECT * FROM foo \
67+
OUTER APPLY (SELECT foo.x + 1) AS bar",
68+
);
69+
}
70+
5571
fn ms() -> TestedDialects {
5672
TestedDialects {
5773
dialects: vec![Box::new(MsSqlDialect {})],

0 commit comments

Comments
 (0)