Skip to content

Commit 52e0f55

Browse files
committed
Support UNION/EXCEPT/INTERSECT
1 parent 533775c commit 52e0f55

File tree

6 files changed

+147
-10
lines changed

6 files changed

+147
-10
lines changed

src/dialect/keywords.rs

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -716,8 +716,7 @@ pub const ALL_KEYWORDS: &'static [&'static str] = &[
716716
/// can be parsed unambiguously without looking ahead.
717717
pub const RESERVED_FOR_TABLE_ALIAS: &'static [&'static str] = &[
718718
// Reserved as both a table and a column alias:
719-
WITH, SELECT, WHERE, GROUP, ORDER,
720-
// TODO add these with tests: UNION, EXCEPT, INTERSECT,
719+
WITH, SELECT, WHERE, GROUP, ORDER, UNION, EXCEPT, INTERSECT,
721720
// Reserved only as a table alias in the `FROM`/`JOIN` clauses:
722721
ON, JOIN, INNER, CROSS, FULL, LEFT, RIGHT, NATURAL, USING,
723722
];
@@ -726,8 +725,7 @@ pub const RESERVED_FOR_TABLE_ALIAS: &'static [&'static str] = &[
726725
/// can be parsed unambiguously without looking ahead.
727726
pub const RESERVED_FOR_COLUMN_ALIAS: &'static [&'static str] = &[
728727
// Reserved as both a table and a column alias:
729-
WITH, SELECT, WHERE, GROUP, ORDER,
730-
// TODO add these with tests: UNION, EXCEPT, INTERSECT,
728+
WITH, SELECT, WHERE, GROUP, ORDER, UNION, EXCEPT, INTERSECT,
731729
// Reserved only as a column alias in the `SELECT` clause:
732730
FROM,
733731
];

src/sqlast/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ mod value;
2222

2323
pub use self::query::{
2424
Cte, Join, JoinConstraint, JoinOperator, SQLOrderByExpr, SQLQuery, SQLSelect, SQLSelectItem,
25-
TableFactor,
25+
SQLSetExpr, SQLSetOperator, TableFactor,
2626
};
2727
pub use self::sqltype::SQLType;
2828
pub use self::table_key::{AlterOperation, Key, TableKey};

src/sqlast/query.rs

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ pub struct SQLQuery {
77
/// WITH (common table expressions, or CTEs)
88
pub ctes: Vec<Cte>,
99
/// SELECT or UNION / EXCEPT / INTECEPT
10-
pub body: SQLSelect,
10+
pub body: SQLSetExpr,
1111
/// ORDER BY
1212
pub order_by: Option<Vec<SQLOrderByExpr>>,
1313
/// LIMIT
@@ -45,6 +45,66 @@ impl ToString for SQLQuery {
4545
}
4646
}
4747

48+
/// A node in a tree, representing a "query body" expression, roughly:
49+
/// `SELECT ... [ {UNION|EXCEPT|INTERSECT} SELECT ...]`
50+
#[derive(Debug, Clone, PartialEq)]
51+
pub enum SQLSetExpr {
52+
/// Restricted SELECT .. FROM .. HAVING (no ORDER BY or set operations)
53+
Select(SQLSelect),
54+
/// Parenthesized SELECT subquery, which may include more set operations
55+
/// in its body and an optional ORDER BY / LIMIT.
56+
Query(Box<SQLQuery>),
57+
/// UNION/EXCEPT/INTERSECT of two queries
58+
SetOperation {
59+
op: SQLSetOperator,
60+
all: bool,
61+
left: Box<SQLSetExpr>,
62+
right: Box<SQLSetExpr>,
63+
},
64+
// TODO: ANSI SQL supports `TABLE` and `VALUES` here.
65+
}
66+
67+
impl ToString for SQLSetExpr {
68+
fn to_string(&self) -> String {
69+
match self {
70+
SQLSetExpr::Select(s) => s.to_string(),
71+
SQLSetExpr::Query(q) => format!("({})", q.to_string()),
72+
SQLSetExpr::SetOperation {
73+
left,
74+
right,
75+
op,
76+
all,
77+
} => {
78+
let all_str = if *all { " ALL" } else { "" };
79+
format!(
80+
"{} {}{} {}",
81+
left.to_string(),
82+
op.to_string(),
83+
all_str,
84+
right.to_string()
85+
)
86+
}
87+
}
88+
}
89+
}
90+
91+
#[derive(Debug, Clone, PartialEq)]
92+
pub enum SQLSetOperator {
93+
Union,
94+
Except,
95+
Intersect,
96+
}
97+
98+
impl ToString for SQLSetOperator {
99+
fn to_string(&self) -> String {
100+
match self {
101+
SQLSetOperator::Union => "UNION".to_string(),
102+
SQLSetOperator::Except => "EXCEPT".to_string(),
103+
SQLSetOperator::Intersect => "INTERSECT".to_string(),
104+
}
105+
}
106+
}
107+
48108
/// A restricted variant of `SELECT` (without CTEs/`ORDER BY`), which may
49109
/// appear either as the only body item of an `SQLQuery`, or as an operand
50110
/// to a set operation like `UNION`.

src/sqlparser.rs

Lines changed: 60 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ impl Parser {
6060
let mut parser = Parser::new(tokens);
6161
let mut stmts = Vec::new();
6262
let mut expecting_statement_delimiter = false;
63+
debug!("Parsing sql '{}'...", sql);
6364
loop {
6465
// ignore empty statements (between successive statement delimiters)
6566
while parser.consume_token(&Token::SemiColon) {
@@ -1208,8 +1209,7 @@ impl Parser {
12081209
vec![]
12091210
};
12101211

1211-
self.expect_keyword("SELECT")?;
1212-
let body = self.parse_select()?;
1212+
let body = self.parse_query_body(0)?;
12131213

12141214
let order_by = if self.parse_keywords(vec!["ORDER", "BY"]) {
12151215
Some(self.parse_order_by_expr_list()?)
@@ -1252,6 +1252,64 @@ impl Parser {
12521252
return Ok(cte);
12531253
}
12541254

1255+
/// Parse a "query body", which is an expression with roughly the
1256+
/// following grammar:
1257+
/// ```text
1258+
/// query_body ::= restricted_select | '(' subquery ')' | set_operation
1259+
/// restricted_select ::= 'SELECT' [expr_list] [ from ] [ where ] [ groupby_having ]
1260+
/// subquery ::= query_body [ order_by_limit ]
1261+
/// set_operation ::= query_body { 'UNION' | 'EXCEPT' | 'INTERSECT' } [ 'ALL' ] query_body
1262+
/// ```
1263+
fn parse_query_body(&mut self, precedence: u8) -> Result<SQLSetExpr, ParserError> {
1264+
// We parse the expression using a Pratt parser, as in `parse_expr()`.
1265+
// Start by parsing a restricted SELECT or a `(subquery)`:
1266+
let mut expr = if self.parse_keyword("SELECT") {
1267+
SQLSetExpr::Select(self.parse_select()?)
1268+
} else if self.consume_token(&Token::LParen) {
1269+
// CTEs are not allowed here, but the parser currently accepts them
1270+
let subquery = self.parse_query()?;
1271+
self.expect_token(&Token::RParen)?;
1272+
SQLSetExpr::Query(Box::new(subquery))
1273+
} else {
1274+
parser_err!("Expected SELECT or a subquery in the query body!")?
1275+
};
1276+
1277+
loop {
1278+
// The query can be optionally followed by a set operator:
1279+
let next_token = self.peek_token();
1280+
let op = self.parse_set_operator(&next_token);
1281+
let next_precedence = match op {
1282+
// UNION and EXCEPT have the same binding power and evaluate left-to-right
1283+
Some(SQLSetOperator::Union) | Some(SQLSetOperator::Except) => 10,
1284+
// INTERSECT has higher precedence than UNION/EXCEPT
1285+
Some(SQLSetOperator::Intersect) => 20,
1286+
// Unexpected token or EOF => stop parsing the query body
1287+
None => break,
1288+
};
1289+
if precedence >= next_precedence {
1290+
break;
1291+
}
1292+
self.next_token(); // skip past the set operator
1293+
expr = SQLSetExpr::SetOperation {
1294+
left: Box::new(expr),
1295+
op: op.unwrap(),
1296+
all: self.parse_keyword("ALL"),
1297+
right: Box::new(self.parse_query_body(next_precedence)?),
1298+
};
1299+
}
1300+
1301+
Ok(expr)
1302+
}
1303+
1304+
fn parse_set_operator(&mut self, token: &Option<Token>) -> Option<SQLSetOperator> {
1305+
match token {
1306+
Some(Token::SQLWord(w)) if w.keyword == "UNION" => Some(SQLSetOperator::Union),
1307+
Some(Token::SQLWord(w)) if w.keyword == "EXCEPT" => Some(SQLSetOperator::Except),
1308+
Some(Token::SQLWord(w)) if w.keyword == "INTERSECT" => Some(SQLSetOperator::Intersect),
1309+
_ => None,
1310+
}
1311+
}
1312+
12551313
/// Parse a restricted `SELECT` statement (no CTEs / `UNION` / `ORDER BY`),
12561314
/// assuming the initial `SELECT` was already consumed
12571315
pub fn parse_select(&mut self) -> Result<SQLSelect, ParserError> {

tests/sqlparser_ansi.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ fn parse_simple_select() {
1212
assert_eq!(1, ast.len());
1313
match ast.first().unwrap() {
1414
SQLStatement::SQLSelect(SQLQuery {
15-
body: SQLSelect { projection, .. },
15+
body: SQLSetExpr::Select(SQLSelect { projection, .. }),
1616
..
1717
}) => {
1818
assert_eq!(3, projection.len());

tests/sqlparser_generic.rs

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -825,6 +825,24 @@ fn parse_derived_tables() {
825825
//TODO: add assertions
826826
}
827827

828+
#[test]
829+
fn parse_union() {
830+
// TODO: add assertions
831+
verified_stmt("SELECT 1 UNION SELECT 2");
832+
verified_stmt("SELECT 1 UNION ALL SELECT 2");
833+
verified_stmt("SELECT 1 EXCEPT SELECT 2");
834+
verified_stmt("SELECT 1 EXCEPT ALL SELECT 2");
835+
verified_stmt("SELECT 1 INTERSECT SELECT 2");
836+
verified_stmt("SELECT 1 INTERSECT ALL SELECT 2");
837+
verified_stmt("SELECT 1 UNION SELECT 2 UNION SELECT 3");
838+
verified_stmt("SELECT 1 EXCEPT SELECT 2 UNION SELECT 3"); // Union[Except[1,2], 3]
839+
verified_stmt("SELECT 1 INTERSECT (SELECT 2 EXCEPT SELECT 3)");
840+
verified_stmt("WITH cte AS (SELECT 1 AS foo) (SELECT foo FROM cte ORDER BY 1 LIMIT 1)");
841+
verified_stmt("SELECT 1 UNION (SELECT 2 ORDER BY 1 LIMIT 1)");
842+
verified_stmt("SELECT 1 UNION SELECT 2 INTERSECT SELECT 3"); // Union[1, Intersect[2,3]]
843+
verified_stmt("SELECT foo FROM tab UNION SELECT bar FROM TAB");
844+
}
845+
828846
#[test]
829847
fn parse_multiple_statements() {
830848
fn test_with(sql1: &str, sql2_kw: &str, sql2_rest: &str) {
@@ -920,7 +938,10 @@ fn expr_from_projection(item: &SQLSelectItem) -> &ASTNode {
920938
}
921939

922940
fn verified_only_select(query: &str) -> SQLSelect {
923-
verified_query(query).body
941+
match verified_query(query).body {
942+
SQLSetExpr::Select(s) => s,
943+
_ => panic!("Expected SQLSetExpr::Select"),
944+
}
924945
}
925946

926947
fn verified_stmt(query: &str) -> SQLStatement {

0 commit comments

Comments
 (0)