support intersect and except syntax (#2882)

This commit is contained in:
yangzhg
2020-02-13 16:48:46 +08:00
committed by GitHub
parent fd492e3b6f
commit ed95352ecd
7 changed files with 269 additions and 169 deletions

View File

@ -23,8 +23,9 @@ import java.util.HashSet;
import java.util.List;
import java.util.Map;
import org.apache.doris.analysis.UnionStmt.Qualifier;
import org.apache.doris.analysis.UnionStmt.UnionOperand;
import org.apache.doris.analysis.SetOperationStmt.Qualifier;
import org.apache.doris.analysis.SetOperationStmt.Operation;
import org.apache.doris.analysis.SetOperationStmt.SetOperand;
import org.apache.doris.catalog.AccessPrivilege;
import org.apache.doris.catalog.AggregateType;
import org.apache.doris.catalog.Column;
@ -200,17 +201,18 @@ terminal String KW_ADD, KW_ADMIN, KW_AFTER, KW_AGGREGATE, KW_ALL, KW_ALTER, KW_A
KW_CONFIG, KW_CONNECTION, KW_CONNECTION_ID, KW_CONSISTENT, KW_COUNT, KW_CREATE, KW_CROSS, KW_CUBE, KW_CURRENT, KW_CURRENT_USER,
KW_DATA, KW_DATABASE, KW_DATABASES, KW_DATE, KW_DATETIME, KW_DAY, KW_DECIMAL, KW_DECOMMISSION, KW_DEFAULT, KW_DESC, KW_DESCRIBE,
KW_DELETE, KW_DISTINCT, KW_DISTINCTPC, KW_DISTINCTPCSA, KW_DISTRIBUTED, KW_DISTRIBUTION, KW_DYNAMIC, KW_BUCKETS, KW_DIV, KW_DOUBLE, KW_DROP, KW_DROPP, KW_DUPLICATE,
KW_ELSE, KW_END, KW_ENGINE, KW_ENGINES, KW_ENTER, KW_ERRORS, KW_EVENTS, KW_EXISTS, KW_EXPORT, KW_EXTERNAL, KW_EXTRACT,
KW_ELSE, KW_END, KW_ENGINE, KW_ENGINES, KW_ENTER, KW_ERRORS, KW_EVENTS, KW_EXCEPT, KW_EXISTS, KW_EXPORT,
KW_EXTERNAL, KW_EXTRACT,
KW_FALSE, KW_FOLLOWER, KW_FOLLOWING, KW_FREE, KW_FROM, KW_FILE, KW_FIRST, KW_FLOAT, KW_FOR, KW_FORMAT, KW_FRONTEND, KW_FRONTENDS, KW_FULL, KW_FUNCTION, KW_FUNCTIONS,
KW_GLOBAL, KW_GRANT, KW_GRANTS, KW_GROUP, KW_GROUPING,
KW_HASH, KW_HAVING, KW_HELP,KW_HLL, KW_HLL_UNION, KW_HOUR, KW_HUB,
KW_IDENTIFIED, KW_IF, KW_IN, KW_INDEX, KW_INDEXES, KW_INFILE,
KW_INNER, KW_INSERT, KW_INT, KW_INTERMEDIATE, KW_INTERVAL, KW_INTO, KW_IS, KW_ISNULL, KW_ISOLATION,
KW_INNER, KW_INSERT, KW_INT, KW_INTERMEDIATE, KW_INTERSECT, KW_INTERVAL, KW_INTO, KW_IS, KW_ISNULL, KW_ISOLATION,
KW_JOIN,
KW_KEY, KW_KILL,
KW_LABEL, KW_LARGEINT, KW_LAST, KW_LEFT, KW_LESS, KW_LEVEL, KW_LIKE, KW_LIMIT, KW_LINK, KW_LOAD,
KW_LOCAL, KW_LOCATION,
KW_MAX, KW_MAX_VALUE, KW_MERGE, KW_MIN, KW_MINUTE, KW_MIGRATE, KW_MIGRATIONS, KW_MODIFY, KW_MONTH,
KW_MAX, KW_MAX_VALUE, KW_MERGE, KW_MIN, KW_MINUTE, KW_MINUS, KW_MIGRATE, KW_MIGRATIONS, KW_MODIFY, KW_MONTH,
KW_NAME, KW_NAMES, KW_NEGATIVE, KW_NO, KW_NOT, KW_NULL, KW_NULLS,
KW_OBSERVER, KW_OFFSET, KW_ON, KW_ONLY, KW_OPEN, KW_OR, KW_ORDER, KW_OUTER, KW_OVER,
KW_PARTITION, KW_PARTITIONS, KW_PASSWORD, KW_PATH, KW_PAUSE, KW_PIPE, KW_PRECEDING,
@ -276,14 +278,14 @@ nonterminal String quantity;
// Description of user
nonterminal UserDesc grant_user;
// Select or union statement.
// Select or set operation(union/intersect/except) statement.
nonterminal QueryStmt query_stmt;
// Single select_stmt or parenthesized query_stmt.
nonterminal QueryStmt union_operand;
// List of select or union blocks connected by UNION operators or a single select block.
nonterminal List<UnionOperand> union_operand_list;
// List of select blocks connected by UNION operators, with order by or limit.
nonterminal QueryStmt union_with_order_by_or_limit;
nonterminal QueryStmt set_operand;
// List of select or set operation(union/intersect/except) blocks connected by set operators or a single select block.
nonterminal List<SetOperand> set_operand_list;
// List of select blocks connected by set operators, with order by or limit.
nonterminal QueryStmt set_operation_with_order_by_or_limit;
nonterminal InsertStmt insert_stmt;
nonterminal InsertTarget insert_target;
nonterminal InsertSource insert_source;
@ -342,7 +344,8 @@ nonterminal JoinOperator join_operator;
nonterminal ArrayList<String> opt_plan_hints;
nonterminal ArrayList<String> opt_sort_hints;
nonterminal Expr sign_chain_expr;
nonterminal Qualifier union_op;
nonterminal Qualifier opt_set_qualifier;
nonterminal Operation set_op;
nonterminal ArrayList<String> opt_common_hints;
nonterminal ArrayList<PartitionName> opt_partition_name_list, partition_name_list;
@ -2406,21 +2409,21 @@ delete_stmt ::=
// even if the union has order by and limit.
// ORDER BY and LIMIT bind to the preceding select statement by default.
query_stmt ::=
opt_with_clause:w union_operand_list:operands
opt_with_clause:w set_operand_list:operands
{:
QueryStmt queryStmt = null;
if (operands.size() == 1) {
queryStmt = operands.get(0).getQueryStmt();
} else {
queryStmt = new UnionStmt(operands, null, LimitElement.NO_LIMIT);
queryStmt = new SetOperationStmt(operands, null, LimitElement.NO_LIMIT);
}
queryStmt.setWithClause(w);
RESULT = queryStmt;
:}
| opt_with_clause:w union_with_order_by_or_limit:union
| opt_with_clause:w set_operation_with_order_by_or_limit:set_operation
{:
union.setWithClause(w);
RESULT = union;
set_operation.setWithClause(w);
RESULT = set_operation;
:}
;
@ -2465,80 +2468,80 @@ with_view_def_list ::=
// making this issue unresolvable.
// We rely on the left precedence of KW_ORDER, KW_BY, and KW_LIMIT,
// to resolve the ambiguity with select_stmt in favor of select_stmt
// (i.e., ORDER BY and LIMIT bind to the select_stmt by default, and not the union).
// There must be at least two union operands for ORDER BY or LIMIT to bind to a union,
// (i.e., ORDER BY and LIMIT bind to the select_stmt by default, and not the set operation).
// There must be at least two set operands for ORDER BY or LIMIT to bind to a set operation,
// and we manually throw a parse error if we reach this production
// with only a single operand.
union_with_order_by_or_limit ::=
union_operand_list:operands
set_operation_with_order_by_or_limit ::=
set_operand_list:operands
KW_LIMIT INTEGER_LITERAL:limit
{:
if (operands.size() == 1) {
parser.parseError("limit", SqlParserSymbols.KW_LIMIT);
}
RESULT = new UnionStmt(operands, null, new LimitElement(limit.longValue()));
RESULT = new SetOperationStmt(operands, null, new LimitElement(limit.longValue()));
:}
|
union_operand_list:operands
set_operand_list:operands
KW_LIMIT INTEGER_LITERAL:offset COMMA INTEGER_LITERAL:limit
{:
if (operands.size() == 1) {
parser.parseError("limit", SqlParserSymbols.KW_LIMIT);
}
RESULT = new UnionStmt(operands, null, new LimitElement(offset.longValue(), limit.longValue()));
RESULT = new SetOperationStmt(operands, null, new LimitElement(offset.longValue(), limit.longValue()));
:}
|
union_operand_list:operands
set_operand_list:operands
KW_LIMIT INTEGER_LITERAL:limit KW_OFFSET INTEGER_LITERAL:offset
{:
if (operands.size() == 1) {
parser.parseError("limit", SqlParserSymbols.KW_LIMIT);
}
RESULT = new UnionStmt(operands, null, new LimitElement(offset.longValue(), limit.longValue()));
RESULT = new SetOperationStmt(operands, null, new LimitElement(offset.longValue(), limit.longValue()));
:}
|
union_operand_list:operands
set_operand_list:operands
KW_ORDER KW_BY order_by_elements:orderByClause
{:
if (operands.size() == 1) {
parser.parseError("order", SqlParserSymbols.KW_ORDER);
}
RESULT = new UnionStmt(operands, orderByClause, LimitElement.NO_LIMIT);
RESULT = new SetOperationStmt(operands, orderByClause, LimitElement.NO_LIMIT);
:}
|
union_operand_list:operands
set_operand_list:operands
KW_ORDER KW_BY order_by_elements:orderByClause
KW_LIMIT INTEGER_LITERAL:limit
{:
if (operands.size() == 1) {
parser.parseError("order", SqlParserSymbols.KW_ORDER);
}
RESULT = new UnionStmt(operands, orderByClause, new LimitElement(limit.longValue()));
RESULT = new SetOperationStmt(operands, orderByClause, new LimitElement(limit.longValue()));
:}
|
union_operand_list:operands
set_operand_list:operands
KW_ORDER KW_BY order_by_elements:orderByClause
KW_LIMIT INTEGER_LITERAL:offset COMMA INTEGER_LITERAL:limit
{:
if (operands.size() == 1) {
parser.parseError("order", SqlParserSymbols.KW_ORDER);
}
RESULT = new UnionStmt(operands, orderByClause, new LimitElement(offset.longValue(), limit.longValue()));
RESULT = new SetOperationStmt(operands, orderByClause, new LimitElement(offset.longValue(), limit.longValue()));
:}
|
union_operand_list:operands
set_operand_list:operands
KW_ORDER KW_BY order_by_elements:orderByClause
KW_LIMIT INTEGER_LITERAL:limit KW_OFFSET INTEGER_LITERAL:offset
{:
if (operands.size() == 1) {
parser.parseError("order", SqlParserSymbols.KW_ORDER);
}
RESULT = new UnionStmt(operands, orderByClause, new LimitElement(offset.longValue(), limit.longValue()));
RESULT = new SetOperationStmt(operands, orderByClause, new LimitElement(offset.longValue(), limit.longValue()));
:}
;
union_operand ::=
set_operand ::=
select_stmt:select
{:
RESULT = select;
@ -2549,26 +2552,36 @@ union_operand ::=
:}
;
union_operand_list ::=
union_operand:operand
set_operand_list ::=
set_operand:operand
{:
List<UnionOperand> operands = new ArrayList<UnionOperand>();
operands.add(new UnionOperand(operand, null));
List<SetOperand> operands = new ArrayList<SetOperand>();
operands.add(new SetOperand(operand, null, null));
RESULT = operands;
:}
| union_operand_list:operands union_op:op union_operand:operand
| set_operand_list:operands set_op:op opt_set_qualifier:qualifier set_operand:operand
{:
operands.add(new UnionOperand(operand, op));
operands.add(new SetOperand(operand, op, qualifier));
RESULT = operands;
:}
;
union_op ::=
set_op ::=
KW_UNION
{: RESULT = Operation.UNION; :}
| KW_INTERSECT
{: RESULT = Operation.INTERSECT; :}
| KW_EXCEPT
{: RESULT = Operation.EXCEPT; :}
| KW_MINUS
{: RESULT = Operation.EXCEPT; :}
;
opt_set_qualifier ::=
{: RESULT = Qualifier.DISTINCT; :}
| KW_UNION KW_DISTINCT
| KW_DISTINCT
{: RESULT = Qualifier.DISTINCT; :}
| KW_UNION KW_ALL
| KW_ALL
{: RESULT = Qualifier.ALL; :}
;

View File

@ -1425,7 +1425,7 @@ public class Analyzer {
* the i-th expr among all expr lists is compatible.
* Throw an AnalysisException if the types are incompatible.
*/
public void castToUnionCompatibleTypes(List<List<Expr>> exprLists)
public void castToSetOpsCompatibleTypes(List<List<Expr>> exprLists)
throws AnalysisException {
if (exprLists == null || exprLists.size() < 2) return;

View File

@ -33,21 +33,27 @@ import java.util.List;
import java.util.Map;
/**
* Representation of a union with its list of operands, and optional order by and limit.
* A union materializes its results, and its resultExprs are SlotRefs into a new
* Representation of a set ops with its list of operands, and optional order by and limit.
* A set ops materializes its results, and its resultExprs are SlotRefs into a new
* materialized tuple.
* During analysis, the operands are normalized (separated into a single sequence of
* DISTINCT followed by a single sequence of ALL operands) and unnested to the extent
* possible. This also creates the AggregationInfo for DISTINCT operands.
*
* Use of resultExprs vs. baseTblResultExprs:
* We consistently use/cast the resultExprs of union operands because the final expr
* We consistently use/cast the resultExprs of set operands because the final expr
* substitution happens during planning. The only place where baseTblResultExprs are
* used is in materializeRequiredSlots() because that is called before plan generation
* and we need to mark the slots of resolved exprs as materialized.
*/
public class UnionStmt extends QueryStmt {
private final static Logger LOG = LogManager.getLogger(UnionStmt.class);
public class SetOperationStmt extends QueryStmt {
private final static Logger LOG = LogManager.getLogger(SetOperationStmt.class);
public enum Operation {
UNION,
INTERSECT,
EXCEPT
}
public enum Qualifier {
ALL,
@ -57,25 +63,25 @@ public class UnionStmt extends QueryStmt {
/////////////////////////////////////////
// BEGIN: Members that need to be reset()
// before analysis, this contains the list of union operands derived verbatim
// before analysis, this contains the list of set operands derived verbatim
// from the query;
// after analysis, this contains all of distinctOperands followed by allOperands
private final List<UnionOperand> operands;
private final List<SetOperand> operands;
// filled during analyze(); contains all operands that need to go through
// distinct aggregation
protected final List<UnionOperand> distinctOperands_ = Lists.newArrayList();
protected final List<SetOperand> distinctOperands_ = Lists.newArrayList();
// filled during analyze(); contains all operands that can be aggregated with
// a simple merge without duplicate elimination (also needs to merge the output
// of the DISTINCT operands)
protected final List<UnionOperand> allOperands_ = Lists.newArrayList();
protected final List<SetOperand> allOperands_ = Lists.newArrayList();
private AggregateInfo distinctAggInfo; // only set if we have DISTINCT ops
private boolean hasDistinct = false;
// Single tuple materialized by the union. Set in analyze().
// Single tuple materialized by the set operation. Set in analyze().
private TupleId tupleId;
// set prior to unnesting
@ -84,15 +90,15 @@ public class UnionStmt extends QueryStmt {
// true if any of the operands_ references an AnalyticExpr
private boolean hasAnalyticExprs_ = false;
// List of output expressions produced by the union without the ORDER BY portion
// List of output expressions produced by the set operation without the ORDER BY portion
// (if any). Same as resultExprs_ if there is no ORDER BY.
private List<Expr> unionResultExprs_ = Lists.newArrayList();
private List<Expr> setOpsResultExprs_ = Lists.newArrayList();
// END: Members that need to be reset()
/////////////////////////////////////////
public UnionStmt(
List<UnionOperand> operands,
public SetOperationStmt(
List<SetOperand> operands,
ArrayList<OrderByElement> orderByElements,
LimitElement limitElement) {
super(orderByElements, limitElement);
@ -102,17 +108,17 @@ public class UnionStmt extends QueryStmt {
/**
* C'tor for cloning.
*/
protected UnionStmt(UnionStmt other) {
protected SetOperationStmt(SetOperationStmt other) {
super(other.cloneOrderByElements(),
(other.limitElement == null) ? null : other.limitElement.clone());
operands = Lists.newArrayList();
if (analyzer != null) {
for (UnionOperand o: other.distinctOperands_) distinctOperands_.add(o.clone());
for (UnionOperand o: other.allOperands_) allOperands_.add(o.clone());
for (SetOperand o: other.distinctOperands_) distinctOperands_.add(o.clone());
for (SetOperand o: other.allOperands_) allOperands_.add(o.clone());
operands.addAll(distinctOperands_);
operands.addAll(allOperands_);
} else {
for (UnionOperand operand: other.operands) operands.add(operand.clone());
for (SetOperand operand: other.operands) operands.add(operand.clone());
}
analyzer = other.analyzer;
distinctAggInfo =
@ -121,11 +127,11 @@ public class UnionStmt extends QueryStmt {
toSqlString = (other.toSqlString != null) ? new String(other.toSqlString) : null;
hasAnalyticExprs_ = other.hasAnalyticExprs_;
withClause_ = (other.withClause_ != null) ? other.withClause_.clone() : null;
unionResultExprs_ = Expr.cloneList(other.unionResultExprs_);
setOpsResultExprs_ = Expr.cloneList(other.setOpsResultExprs_);
}
@Override
public UnionStmt clone() { return new UnionStmt(this); }
public SetOperationStmt clone() { return new SetOperationStmt(this); }
/**
* Undoes all changes made by analyze() except distinct propagation and unnesting.
@ -137,20 +143,20 @@ public class UnionStmt extends QueryStmt {
@Override
public void reset() {
super.reset();
for (UnionOperand op: operands) op.reset();
for (SetOperand op: operands) op.reset();
distinctOperands_.clear();
allOperands_.clear();
distinctAggInfo = null;
tupleId = null;
toSqlString = null;
hasAnalyticExprs_ = false;
unionResultExprs_.clear();
setOpsResultExprs_.clear();
}
public List<UnionOperand> getOperands() { return operands; }
public List<UnionOperand> getDistinctOperands() { return distinctOperands_; }
public List<SetOperand> getOperands() { return operands; }
public List<SetOperand> getDistinctOperands() { return distinctOperands_; }
public boolean hasDistinctOps() { return !distinctOperands_.isEmpty(); }
public List<UnionOperand> getAllOperands() { return allOperands_; }
public List<SetOperand> getAllOperands() { return allOperands_; }
public boolean hasAllOps() { return !allOperands_.isEmpty(); }
public AggregateInfo getDistinctAggInfo() { return distinctAggInfo; }
public boolean hasAnalyticExprs() { return hasAnalyticExprs_; }
@ -161,24 +167,19 @@ public class UnionStmt extends QueryStmt {
allOperands_.clear();
}
public List<UnionOperand> getUnionOperands() {
return operands;
}
public List<Expr> getUnionResultExprs() { return unionResultExprs_; }
public List<Expr> getSetOpsResultExprs() { return setOpsResultExprs_; }
@Override
public void getDbs(Analyzer analyzer, Map<String, Database> dbs) throws AnalysisException {
getWithClauseDbs(analyzer, dbs);
for (UnionOperand op : operands) {
for (SetOperand op : operands) {
op.getQueryStmt().getDbs(analyzer, dbs);
}
}
/**
* Propagates DISTINCT from left to right, and checks that all
* union operands are union compatible, adding implicit casts if necessary.
* set operands are set compatible, adding implicit casts if necessary.
*/
@Override
public void analyze(Analyzer analyzer) throws AnalysisException, UserException {
@ -186,6 +187,11 @@ public class UnionStmt extends QueryStmt {
super.analyze(analyzer);
Preconditions.checkState(operands.size() > 0);
for (SetOperand op : operands) {
if (op.getOperation() != null && op.getOperation() != Operation.UNION) {
throw new AnalysisException("INTERSECT/EXCEPT is not implemented yet.");
}
}
// Propagates DISTINCT from left to right,
propagateDistinct();
@ -204,7 +210,7 @@ public class UnionStmt extends QueryStmt {
// Compute hasAnalyticExprs_
hasAnalyticExprs_ = false;
for (UnionOperand op: operands) {
for (SetOperand op: operands) {
if (op.hasAnalyticExprs()) {
hasAnalyticExprs_ = true;
break;
@ -213,33 +219,33 @@ public class UnionStmt extends QueryStmt {
// Collect all result expr lists and cast the exprs as necessary.
List<List<Expr>> resultExprLists = Lists.newArrayList();
for (UnionOperand op: operands) {
for (SetOperand op: operands) {
resultExprLists.add(op.getQueryStmt().getResultExprs());
}
analyzer.castToUnionCompatibleTypes(resultExprLists);
analyzer.castToSetOpsCompatibleTypes(resultExprLists);
// Create tuple descriptor materialized by this UnionStmt, its resultExprs, and
// Create tuple descriptor materialized by this SetOperationStmt, its resultExprs, and
// its sortInfo if necessary.
createMetadata(analyzer);
createSortInfo(analyzer);
// Create unnested operands' smaps.
for (UnionOperand operand: operands) setOperandSmap(operand, analyzer);
for (SetOperand operand: operands) setOperandSmap(operand, analyzer);
// Create distinctAggInfo, if necessary.
if (!distinctOperands_.isEmpty()) {
// Aggregate produces exactly the same tuple as the original union stmt.
// Aggregate produces exactly the same tuple as the original setOp stmt.
ArrayList<Expr> groupingExprs = Expr.cloneList(resultExprs);
try {
distinctAggInfo = AggregateInfo.create(
groupingExprs, null, analyzer.getDescTbl().getTupleDesc(tupleId), analyzer);
} catch (AnalysisException e) {
// Should never happen.
throw new IllegalStateException("Error creating agg info in UnionStmt.analyze()", e);
throw new IllegalStateException("Error creating agg info in SetOperationStmt.analyze()", e);
}
}
unionResultExprs_ = Expr.cloneList(resultExprs);
setOpsResultExprs_ = Expr.cloneList(resultExprs);
if (evaluateOrderBy) createSortTupleInfo(analyzer);
baseTblResultExprs = resultExprs;
}
@ -266,7 +272,7 @@ public class UnionStmt extends QueryStmt {
}
/**
* Fill distinct-/allOperands and performs possible unnesting of UnionStmt
* Fill distinct-/allOperands and performs possible unnesting of SetOperationStmt
* operands in the process.
*/
private void unnestOperands(Analyzer analyzer) throws AnalysisException {
@ -279,7 +285,7 @@ public class UnionStmt extends QueryStmt {
// find index of first ALL operand
int firstUnionAllIdx = operands.size();
for (int i = 1; i < operands.size(); ++i) {
UnionOperand operand = operands.get(i);
SetOperand operand = operands.get(i);
if (operand.getQualifier() == Qualifier.ALL) {
firstUnionAllIdx = (i == 1 ? 0 : i);
break;
@ -301,8 +307,8 @@ public class UnionStmt extends QueryStmt {
unnestOperand(allOperands_, Qualifier.ALL, operands.get(i));
}
for (UnionOperand op: distinctOperands_) op.setQualifier(Qualifier.DISTINCT);
for (UnionOperand op: allOperands_) op.setQualifier(Qualifier.ALL);
for (SetOperand op: distinctOperands_) op.setQualifier(Qualifier.DISTINCT);
for (SetOperand op: allOperands_) op.setQualifier(Qualifier.ALL);
operands.clear();
operands.addAll(distinctOperands_);
@ -310,11 +316,11 @@ public class UnionStmt extends QueryStmt {
}
/**
* Add a single operand to the target list; if the operand itself is a UnionStmt, apply
* Add a single operand to the target list; if the operand itself is a SetOperationStmt, apply
* unnesting to the extent possible (possibly modifying 'operand' in the process).
*/
private void unnestOperand(
List<UnionOperand> target, Qualifier targetQualifier, UnionOperand operand) {
List<SetOperand> target, Qualifier targetQualifier, SetOperand operand) {
Preconditions.checkState(operand.isAnalyzed());
QueryStmt queryStmt = operand.getQueryStmt();
if (queryStmt instanceof SelectStmt) {
@ -322,30 +328,30 @@ public class UnionStmt extends QueryStmt {
return;
}
Preconditions.checkState(queryStmt instanceof UnionStmt);
UnionStmt unionStmt = (UnionStmt) queryStmt;
if (unionStmt.hasLimit() || unionStmt.hasOffset()) {
// we must preserve the nested Union
Preconditions.checkState(queryStmt instanceof SetOperationStmt);
SetOperationStmt setOperationStmt = (SetOperationStmt) queryStmt;
if (setOperationStmt.hasLimit() || setOperationStmt.hasOffset()) {
// we must preserve the nested SetOps
target.add(operand);
} else if (targetQualifier == Qualifier.DISTINCT || !unionStmt.hasDistinctOps()) {
// there is no limit in the nested Union and we can absorb all of its
} else if (targetQualifier == Qualifier.DISTINCT || !setOperationStmt.hasDistinctOps()) {
// there is no limit in the nested SetOps and we can absorb all of its
// operands as-is
target.addAll(unionStmt.getDistinctOperands());
target.addAll(unionStmt.getAllOperands());
target.addAll(setOperationStmt.getDistinctOperands());
target.addAll(setOperationStmt.getAllOperands());
} else {
// the nested Union contains some Distinct ops and we're accumulating
// the nested SetOps contains some Distinct ops and we're accumulating
// into our All ops; unnest only the All ops and leave the rest in place
target.addAll(unionStmt.getAllOperands());
unionStmt.removeAllOperands();
target.addAll(setOperationStmt.getAllOperands());
setOperationStmt.removeAllOperands();
target.add(operand);
}
}
/**
* Sets the smap for the given operand. It maps from the output slots this union's
* Sets the smap for the given operand. It maps from the output slots this SetOps's
* tuple to the corresponding result exprs of the operand.
*/
private void setOperandSmap(UnionOperand operand, Analyzer analyzer) {
private void setOperandSmap(SetOperand operand, Analyzer analyzer) {
TupleDescriptor tupleDesc = analyzer.getDescTbl().getTupleDesc(tupleId);
// operands' smaps were already set in the operands' analyze()
operand.getSmap().clear();
@ -376,7 +382,7 @@ public class UnionStmt extends QueryStmt {
private void propagateDistinct() {
int firstDistinctPos = -1;
for (int i = operands.size() - 1; i > 0; --i) {
UnionOperand operand = operands.get(i);
SetOperand operand = operands.get(i);
if (firstDistinctPos != -1) {
// There is a DISTINCT somewhere to the right.
operand.setQualifier(Qualifier.DISTINCT);
@ -387,18 +393,18 @@ public class UnionStmt extends QueryStmt {
}
/**
* Create a descriptor for the tuple materialized by the union.
* Create a descriptor for the tuple materialized by the setOps.
* Set resultExprs to be slot refs into that tuple.
* Also fills the substitution map, such that "order by" can properly resolve
* column references from the result of the union.
* column references from the result of the setOps.
*/
private void createMetadata(Analyzer analyzer) throws AnalysisException {
// Create tuple descriptor for materialized tuple created by the union.
TupleDescriptor tupleDesc = analyzer.getDescTbl().createTupleDescriptor("union");
// Create tuple descriptor for materialized tuple created by the setOps.
TupleDescriptor tupleDesc = analyzer.getDescTbl().createTupleDescriptor("SetOps");
tupleDesc.setIsMaterialized(true);
tupleId = tupleDesc.getId();
if (LOG.isTraceEnabled()) {
LOG.trace("UnionStmt.createMetadata: tupleId=" + tupleId.toString());
LOG.trace("SetOperationStmt.createMetadata: tupleId=" + tupleId.toString());
}
// One slot per expr in the select blocks. Use first select block as representative.
@ -447,7 +453,7 @@ public class UnionStmt extends QueryStmt {
// to operands' result exprs (if those happen to be slotrefs);
// don't do that if the operand computes analytic exprs
// (see Planner.createInlineViewPlan() for the reasoning)
for (UnionOperand op: operands) {
for (SetOperand op: operands) {
Expr resultExpr = op.getQueryStmt().getResultExprs().get(i);
slotDesc.addSourceExpr(resultExpr);
SlotRef slotRef = resultExpr.unwrapSlotRef(false);
@ -458,7 +464,7 @@ public class UnionStmt extends QueryStmt {
if (slotRef == null) continue;
// analyzer.registerValueTransfer(outputSlotRef.getSlotId(), slotRef.getSlotId());
}
// If all the child slots are not nullable, then the union output slot should not
// If all the child slots are not nullable, then the SetOps output slot should not
// be nullable as well.
slotDesc.setIsNullable(isNullable);
}
@ -486,7 +492,7 @@ public class UnionStmt extends QueryStmt {
for (int i = 0; i < outputSlots.size(); ++i) {
SlotDescriptor slotDesc = outputSlots.get(i);
if (!slotDesc.isMaterialized()) continue;
for (UnionOperand op: operands) {
for (SetOperand op: operands) {
exprs.add(op.getQueryStmt().getBaseTblResultExprs().get(i));
}
if (distinctAggInfo != null) {
@ -497,14 +503,14 @@ public class UnionStmt extends QueryStmt {
}
materializeSlots(analyzer, exprs);
for (UnionOperand op: operands) {
for (SetOperand op: operands) {
op.getQueryStmt().materializeRequiredSlots(analyzer);
}
}
@Override
public void rewriteExprs(ExprRewriter rewriter) throws AnalysisException {
for (UnionOperand op: operands) op.getQueryStmt().rewriteExprs(rewriter);
for (SetOperand op: operands) op.getQueryStmt().rewriteExprs(rewriter);
if (orderByElements != null) {
for (OrderByElement orderByElem: orderByElements) {
orderByElem.setExpr(rewriter.rewrite(orderByElem.getExpr(), analyzer));
@ -524,7 +530,7 @@ public class UnionStmt extends QueryStmt {
@Override
public void collectTableRefs(List<TableRef> tblRefs) {
for (UnionOperand op: operands) op.getQueryStmt().collectTableRefs(tblRefs);
for (SetOperand op: operands) op.getQueryStmt().collectTableRefs(tblRefs);
}
@Override
@ -537,20 +543,22 @@ public class UnionStmt extends QueryStmt {
strBuilder.append(operands.get(0).getQueryStmt().toSql());
for (int i = 1; i < operands.size() - 1; ++i) {
strBuilder.append(
" UNION " + ((operands.get(i).getQualifier() == Qualifier.ALL) ? "ALL " : ""));
if (operands.get(i).getQueryStmt() instanceof UnionStmt) {
" " + operands.get(i).getOperation().toString() + " "
+ ((operands.get(i).getQualifier() == Qualifier.ALL) ? "ALL " : ""));
if (operands.get(i).getQueryStmt() instanceof SetOperationStmt) {
strBuilder.append("(");
}
strBuilder.append(operands.get(i).getQueryStmt().toSql());
if (operands.get(i).getQueryStmt() instanceof UnionStmt) {
if (operands.get(i).getQueryStmt() instanceof SetOperationStmt) {
strBuilder.append(")");
}
}
// Determine whether we need parenthesis around the last union operand.
UnionOperand lastOperand = operands.get(operands.size() - 1);
// Determine whether we need parenthesis around the last Set operand.
SetOperand lastOperand = operands.get(operands.size() - 1);
QueryStmt lastQueryStmt = lastOperand.getQueryStmt();
strBuilder.append(" UNION " + ((lastOperand.getQualifier() == Qualifier.ALL) ? "ALL " : ""));
if (lastQueryStmt instanceof UnionStmt || ((hasOrderByClause() || hasLimitClause()) &&
strBuilder.append(" " + lastOperand.getOperation().toString() + " "
+ ((lastOperand.getQualifier() == Qualifier.ALL) ? "ALL " : ""));
if (lastQueryStmt instanceof SetOperationStmt || ((hasOrderByClause() || hasLimitClause()) &&
!lastQueryStmt.hasLimitClause() &&
!lastQueryStmt.hasOrderByClause())) {
strBuilder.append("(");
@ -584,7 +592,7 @@ public class UnionStmt extends QueryStmt {
@Override
public void setNeedToSql(boolean needToSql) {
super.setNeedToSql(needToSql);
for (UnionOperand operand : operands) {
for (SetOperand operand : operands) {
operand.getQueryStmt().setNeedToSql(needToSql);
}
}
@ -601,14 +609,17 @@ public class UnionStmt extends QueryStmt {
}
/**
* Represents an operand to a union. It consists of a query statement and its left
* Represents an operand to a SetOperand. It consists of a query statement and its left
* all/distinct qualifier (null for the first operand).
*/
public static class UnionOperand {
public static class SetOperand {
// Operand indicate this SetOperand is union/intersect/except
private Operation operation;
// Effective qualifier. Should not be reset() to preserve changes made during
// distinct propagation and unnesting that are needed after rewriting Subqueries.
private Qualifier qualifier_;
// ///////////////////////////////////////
// BEGIN: Members that need to be reset()
@ -618,14 +629,15 @@ public class UnionStmt extends QueryStmt {
// We must preserve the conjuncts registered in the analyzer for partition pruning.
private Analyzer analyzer;
// Map from UnionStmt's result slots to our resultExprs. Used during plan generation.
// Map from SetOperationStmt's result slots to our resultExprs. Used during plan generation.
private final ExprSubstitutionMap smap_;
// END: Members that need to be reset()
// ///////////////////////////////////////
public UnionOperand(QueryStmt queryStmt, Qualifier qualifier) {
public SetOperand(QueryStmt queryStmt, Operation operation, Qualifier qualifier) {
this.queryStmt = queryStmt;
this.operation = operation;
qualifier_ = qualifier;
smap_ = new ExprSubstitutionMap();
}
@ -639,8 +651,15 @@ public class UnionStmt extends QueryStmt {
public boolean isAnalyzed() { return analyzer != null; }
public QueryStmt getQueryStmt() { return queryStmt; }
public Qualifier getQualifier() { return qualifier_; }
public Operation getOperation() {
return operation;
}
// Used for propagating DISTINCT.
public void setQualifier(Qualifier qualifier) { qualifier_ = qualifier; }
public void setOperation(Operation operation) {
this.operation =operation;
}
public Analyzer getAnalyzer() { return analyzer; }
public ExprSubstitutionMap getSmap() { return smap_; }
@ -648,16 +667,17 @@ public class UnionStmt extends QueryStmt {
if (queryStmt instanceof SelectStmt) {
return ((SelectStmt) queryStmt).hasAnalyticInfo();
} else {
Preconditions.checkState(queryStmt instanceof UnionStmt);
return ((UnionStmt) queryStmt).hasAnalyticExprs();
Preconditions.checkState(queryStmt instanceof SetOperationStmt);
return ((SetOperationStmt) queryStmt).hasAnalyticExprs();
}
}
/**
* C'tor for cloning.
*/
private UnionOperand(UnionOperand other) {
private SetOperand(SetOperand other) {
queryStmt = other.queryStmt.clone();
this.operation = other.operation;
qualifier_ = other.qualifier_;
analyzer = other.analyzer;
smap_ = other.smap_.clone();
@ -670,8 +690,8 @@ public class UnionStmt extends QueryStmt {
}
@Override
public UnionOperand clone() {
return new UnionOperand(this);
public SetOperand clone() {
return new SetOperand(this);
}
}
}

View File

@ -69,8 +69,8 @@ public class StmtRewriter {
Preconditions.checkNotNull(stmt);
if (stmt instanceof SelectStmt) {
rewriteSelectStatement((SelectStmt) stmt, analyzer);
} else if (stmt instanceof UnionStmt) {
rewriteUnionStatement((UnionStmt) stmt, analyzer);
} else if (stmt instanceof SetOperationStmt) {
rewriteUnionStatement((SetOperationStmt) stmt, analyzer);
} else {
throw new AnalysisException("Subqueries not supported for "
+ stmt.getClass().getSimpleName() + " statements");
@ -105,9 +105,9 @@ public class StmtRewriter {
* Rewrite all operands in a UNION. The conditions that apply to SelectStmt rewriting
* also apply here.
*/
private static void rewriteUnionStatement(UnionStmt stmt, Analyzer analyzer)
private static void rewriteUnionStatement(SetOperationStmt stmt, Analyzer analyzer)
throws AnalysisException {
for (UnionStmt.UnionOperand operand: stmt.getOperands()) {
for (SetOperationStmt.SetOperand operand: stmt.getOperands()) {
Preconditions.checkState(operand.getQueryStmt() instanceof SelectStmt);
StmtRewriter.rewriteSelectStatement(
(SelectStmt)operand.getQueryStmt(), operand.getAnalyzer());

View File

@ -39,6 +39,7 @@ import org.apache.doris.analysis.LiteralExpr;
import org.apache.doris.analysis.NullLiteral;
import org.apache.doris.analysis.QueryStmt;
import org.apache.doris.analysis.SelectStmt;
import org.apache.doris.analysis.SetOperationStmt;
import org.apache.doris.analysis.SlotDescriptor;
import org.apache.doris.analysis.SlotId;
import org.apache.doris.analysis.SlotRef;
@ -46,7 +47,6 @@ import org.apache.doris.analysis.TableRef;
import org.apache.doris.analysis.TupleDescriptor;
import org.apache.doris.analysis.TupleId;
import org.apache.doris.analysis.TupleIsNullPredicate;
import org.apache.doris.analysis.UnionStmt;
import org.apache.doris.catalog.AggregateFunction;
import org.apache.doris.catalog.AggregateType;
import org.apache.doris.catalog.Column;
@ -235,8 +235,8 @@ public class SingleNodePlanner {
}
}
} else {
Preconditions.checkState(stmt instanceof UnionStmt);
root = createUnionPlan((UnionStmt) stmt, analyzer, newDefaultOrderByLimit);
Preconditions.checkState(stmt instanceof SetOperationStmt);
root = createSetOperationPlan((SetOperationStmt) stmt, analyzer, newDefaultOrderByLimit);
}
// Avoid adding a sort node if the sort tuple has no materialized slots.
@ -1175,8 +1175,8 @@ public class SingleNodePlanner {
viewAnalyzer.registerConjuncts(newConjuncts, select.getTableRefs().get(0).getDesc().getId().asList());
}
} else {
Preconditions.checkArgument(stmt instanceof UnionStmt);
final UnionStmt union = (UnionStmt) stmt;
Preconditions.checkArgument(stmt instanceof SetOperationStmt);
final SetOperationStmt union = (SetOperationStmt) stmt;
viewAnalyzer.registerConjuncts(newConjuncts, union.getTupleId().asList());
}
}
@ -1204,11 +1204,11 @@ public class SingleNodePlanner {
// UnionNode will handle predicates and assigns predicates to it's children.
final List<Expr> candicatePredicates =
Expr.substituteList(viewPredicates, inlineViewRef.getSmap(), analyzer, false);
if (inlineViewRef.getViewStmt() instanceof UnionStmt) {
final UnionStmt unionStmt = (UnionStmt) inlineViewRef.getViewStmt();
if (inlineViewRef.getViewStmt() instanceof SetOperationStmt) {
final SetOperationStmt setOperationStmt = (SetOperationStmt) inlineViewRef.getViewStmt();
for (int i = 0; i < candicatePredicates.size(); i++) {
final Expr predicate = candicatePredicates.get(i);
if (predicate.isBound(unionStmt.getTupleId())) {
if (predicate.isBound(setOperationStmt.getTupleId())) {
pushDownPredicates.add(predicate);
} else {
pushDownFailedPredicates.add(viewPredicates.get(i));
@ -1483,12 +1483,12 @@ public class SingleNodePlanner {
* as a child of the returned UnionNode.
*/
private UnionNode createUnionPlan(
Analyzer analyzer, UnionStmt unionStmt, List<UnionStmt.UnionOperand> unionOperands,
Analyzer analyzer, SetOperationStmt setOperationStmt, List<SetOperationStmt.SetOperand> setOperands,
PlanNode unionDistinctPlan, long defaultOrderByLimit)
throws UserException, AnalysisException {
UnionNode unionNode = new UnionNode(ctx_.getNextNodeId(), unionStmt.getTupleId(),
unionStmt.getUnionResultExprs(), false);
for (UnionStmt.UnionOperand op : unionOperands) {
UnionNode unionNode = new UnionNode(ctx_.getNextNodeId(), setOperationStmt.getTupleId(),
setOperationStmt.getSetOpsResultExprs(), false);
for (SetOperationStmt.SetOperand op : setOperands) {
if (op.getAnalyzer().hasEmptyResultSet()) {
unmarkCollectionSlots(op.getQueryStmt());
continue;
@ -1512,10 +1512,10 @@ public class SingleNodePlanner {
}
if (unionDistinctPlan != null) {
Preconditions.checkState(unionStmt.hasDistinctOps());
Preconditions.checkState(setOperationStmt.hasDistinctOps());
Preconditions.checkState(unionDistinctPlan instanceof AggregationNode);
unionNode.addChild(unionDistinctPlan,
unionStmt.getDistinctAggInfo().getGroupingExprs());
setOperationStmt.getDistinctAggInfo().getGroupingExprs());
}
unionNode.init(analyzer);
return unionNode;
@ -1537,23 +1537,23 @@ public class SingleNodePlanner {
* TODO: Simplify the plan of unions with only a single non-empty operand to not
* use a union node (this is tricky because a union materializes a new tuple).
*/
private PlanNode createUnionPlan(UnionStmt unionStmt, Analyzer analyzer, long defaultOrderByLimit)
private PlanNode createSetOperationPlan(SetOperationStmt setOperationStmt, Analyzer analyzer, long defaultOrderByLimit)
throws UserException, AnalysisException {
// TODO(zc): get unassigned conjuncts
// List<Expr> conjuncts =
// analyzer.getUnassignedConjuncts(unionStmt.getTupleId().asList(), false);
List<Expr> conjuncts = analyzer.getUnassignedConjuncts(unionStmt.getTupleId().asList());
List<Expr> conjuncts = analyzer.getUnassignedConjuncts(setOperationStmt.getTupleId().asList());
// TODO chenhao
// Because Conjuncts can't be assigned to UnionNode and Palo's fe can't evaluate conjuncts,
// it needs to add SelectNode as UnionNode's parent, when UnionStmt's Ops contains constant
// Select.
boolean hasConstantOp = false;
if (!unionStmt.hasAnalyticExprs()) {
if (!setOperationStmt.hasAnalyticExprs()) {
// Turn unassigned predicates for unionStmt's tupleId_ into predicates for
// the individual operands.
// Do this prior to creating the operands' plan trees so they get a chance to
// pick up propagated predicates.
for (UnionStmt.UnionOperand op : unionStmt.getOperands()) {
for (SetOperationStmt.SetOperand op : setOperationStmt.getOperands()) {
List<Expr> opConjuncts =
Expr.substituteList(conjuncts, op.getSmap(), analyzer, false);
boolean selectHasTableRef = true;
@ -1570,8 +1570,8 @@ public class SingleNodePlanner {
if ((queryStmt instanceof SelectStmt) && selectHasTableRef) {
final SelectStmt select = (SelectStmt) queryStmt;
op.getAnalyzer().registerConjuncts(opConjuncts, select.getTableRefIds());
} else if (queryStmt instanceof UnionStmt) {
final UnionStmt union = (UnionStmt) queryStmt;
} else if (queryStmt instanceof SetOperationStmt) {
final SetOperationStmt union = (SetOperationStmt) queryStmt;
op.getAnalyzer().registerConjuncts(opConjuncts, union.getTupleId().asList());
} else {
if (selectHasTableRef) {
@ -1587,24 +1587,24 @@ public class SingleNodePlanner {
analyzer.materializeSlots(conjuncts);
}
// mark slots after predicate propagation but prior to plan tree generation
unionStmt.materializeRequiredSlots(analyzer);
setOperationStmt.materializeRequiredSlots(analyzer);
PlanNode result = null;
// create DISTINCT tree
if (unionStmt.hasDistinctOps()) {
if (setOperationStmt.hasDistinctOps()) {
result = createUnionPlan(
analyzer, unionStmt, unionStmt.getDistinctOperands(), null, defaultOrderByLimit);
result = new AggregationNode(ctx_.getNextNodeId(), result, unionStmt.getDistinctAggInfo());
analyzer, setOperationStmt, setOperationStmt.getDistinctOperands(), null, defaultOrderByLimit);
result = new AggregationNode(ctx_.getNextNodeId(), result, setOperationStmt.getDistinctAggInfo());
result.init(analyzer);
}
// create ALL tree
if (unionStmt.hasAllOps()) {
result = createUnionPlan(analyzer, unionStmt, unionStmt.getAllOperands(), result, defaultOrderByLimit);
if (setOperationStmt.hasAllOps()) {
result = createUnionPlan(analyzer, setOperationStmt, setOperationStmt.getAllOperands(), result, defaultOrderByLimit);
}
if (unionStmt.hasAnalyticExprs() || hasConstantOp) {
if (setOperationStmt.hasAnalyticExprs() || hasConstantOp) {
result = addUnassignedConjuncts(
analyzer, unionStmt.getTupleId().asList(), result);
analyzer, setOperationStmt.getTupleId().asList(), result);
}
return result;
}

View File

@ -171,6 +171,7 @@ import org.apache.doris.qe.SqlModeHelper;
keywordMap.put("enter", new Integer(SqlParserSymbols.KW_ENTER));
keywordMap.put("errors", new Integer(SqlParserSymbols.KW_ERRORS));
keywordMap.put("events", new Integer(SqlParserSymbols.KW_EVENTS));
keywordMap.put("except", new Integer(SqlParserSymbols.KW_EXCEPT));
keywordMap.put("exists", new Integer(SqlParserSymbols.KW_EXISTS));
keywordMap.put("explain", new Integer(SqlParserSymbols.KW_DESCRIBE));
keywordMap.put("export", new Integer(SqlParserSymbols.KW_EXPORT));
@ -214,6 +215,7 @@ import org.apache.doris.qe.SqlModeHelper;
keywordMap.put("int", new Integer(SqlParserSymbols.KW_INT));
keywordMap.put("integer", new Integer(SqlParserSymbols.KW_INT));
keywordMap.put("intermediate", new Integer(SqlParserSymbols.KW_INTERMEDIATE));
keywordMap.put("intersect", new Integer(SqlParserSymbols.KW_INTERSECT));
keywordMap.put("interval", new Integer(SqlParserSymbols.KW_INTERVAL));
keywordMap.put("into", new Integer(SqlParserSymbols.KW_INTO));
keywordMap.put("is", new Integer(SqlParserSymbols.KW_IS));
@ -240,6 +242,7 @@ import org.apache.doris.qe.SqlModeHelper;
keywordMap.put("migrate", new Integer(SqlParserSymbols.KW_MIGRATE));
keywordMap.put("migrations", new Integer(SqlParserSymbols.KW_MIGRATIONS));
keywordMap.put("min", new Integer(SqlParserSymbols.KW_MIN));
keywordMap.put("minus", new Integer(SqlParserSymbols.KW_MINUS));
keywordMap.put("minute", new Integer(SqlParserSymbols.KW_MINUTE));
keywordMap.put("modify", new Integer(SqlParserSymbols.KW_MODIFY));
keywordMap.put("month", new Integer(SqlParserSymbols.KW_MONTH));

View File

@ -0,0 +1,64 @@
package org.apache.doris.analysis;
import java.io.StringReader;
import org.apache.doris.common.AnalysisException;
import org.apache.doris.mysql.privilege.MockedAuth;
import org.apache.doris.mysql.privilege.PaloAuth;
import org.apache.doris.qe.ConnectContext;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import mockit.Mocked;
public class SetOperationStmtTest {
private Analyzer analyzer;
@Mocked
private PaloAuth auth;
@Mocked
private ConnectContext ctx;
@Before
public void setUp() {
analyzer = AccessTestUtil.fetchAdminAnalyzer(true);
MockedAuth.mockedAuth(auth);
MockedAuth.mockedConnectContext(ctx, "root", "192.168.1.1");
}
@Test
public void testNormal() throws Exception {
String sql = "select k1,k2 from t where k1='a' union select k1,k2 from t where k1='b';";
SqlScanner input = new SqlScanner(new StringReader(sql));
SqlParser parser = new SqlParser(input);
SetOperationStmt stmt = (SetOperationStmt) parser.parse().value;
Assert.assertEquals(SetOperationStmt.Operation.UNION, stmt.getOperands().get(1).getOperation());
sql = "select k1,k2 from t where k1='a' intersect select k1,k2 from t where k1='b';";
input = new SqlScanner(new StringReader(sql));
parser = new SqlParser(input);
stmt = (SetOperationStmt) parser.parse().value;
Assert.assertEquals(SetOperationStmt.Operation.INTERSECT, stmt.getOperands().get(1).getOperation());
sql = "select k1,k2 from t where k1='a' except select k1,k2 from t where k1='b';";
input = new SqlScanner(new StringReader(sql));
parser = new SqlParser(input);
stmt = (SetOperationStmt) parser.parse().value;
Assert.assertEquals(SetOperationStmt.Operation.EXCEPT, stmt.getOperands().get(1).getOperation());
sql = "select k1,k2 from t where k1='a' minus select k1,k2 from t where k1='b';";
input = new SqlScanner(new StringReader(sql));
parser = new SqlParser(input);
stmt = (SetOperationStmt) parser.parse().value;
Assert.assertEquals(SetOperationStmt.Operation.EXCEPT, stmt.getOperands().get(1).getOperation());
sql = "select k1,k2 from t where k1='a' union select k1,k2 from t where k1='b' intersect select k1,k2 from t "
+ "where k1='c' except select k1,k2 from t where k1='d';";
input = new SqlScanner(new StringReader(sql));
parser = new SqlParser(input);
stmt = (SetOperationStmt) parser.parse().value;
Assert.assertEquals(SetOperationStmt.Operation.UNION, stmt.getOperands().get(1).getOperation());
Assert.assertEquals(SetOperationStmt.Operation.INTERSECT, stmt.getOperands().get(2).getOperation());
Assert.assertEquals(SetOperationStmt.Operation.EXCEPT, stmt.getOperands().get(3).getOperation());
Assert.assertEquals(4, stmt.getOperands().size());
}
}