blob: b99f74f4eac1837abf5d64043f392cc0a5c41bf6 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.phoenix.execute;
import static org.apache.phoenix.util.NumberUtil.add;
import static org.apache.phoenix.util.NumberUtil.getMin;
import java.io.IOException;
import java.sql.ParameterMetaData;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import org.apache.hadoop.hbase.client.Scan;
import org.apache.hadoop.hbase.io.ImmutableBytesWritable;
import org.apache.hadoop.hbase.util.Pair;
import org.apache.phoenix.compile.ColumnResolver;
import org.apache.phoenix.compile.ExplainPlan;
import org.apache.phoenix.compile.GroupByCompiler.GroupBy;
import org.apache.phoenix.compile.OrderByCompiler.OrderBy;
import org.apache.phoenix.compile.QueryPlan;
import org.apache.phoenix.compile.RowProjector;
import org.apache.phoenix.compile.StatelessExpressionCompiler;
import org.apache.phoenix.compile.StatementContext;
import org.apache.phoenix.exception.PhoenixIOException;
import org.apache.phoenix.exception.SQLExceptionCode;
import org.apache.phoenix.exception.SQLExceptionInfo;
import org.apache.phoenix.execute.TupleProjector.ProjectedValueTuple;
import org.apache.phoenix.execute.visitor.ByteCountVisitor;
import org.apache.phoenix.execute.visitor.QueryPlanVisitor;
import org.apache.phoenix.expression.Expression;
import org.apache.phoenix.expression.OrderByExpression;
import org.apache.phoenix.iterate.DefaultParallelScanGrouper;
import org.apache.phoenix.iterate.ParallelScanGrouper;
import org.apache.phoenix.iterate.PhoenixQueues;
import org.apache.phoenix.iterate.ResultIterator;
import org.apache.phoenix.iterate.SizeAwareQueue;
import org.apache.phoenix.jdbc.PhoenixParameterMetaData;
import org.apache.phoenix.jdbc.PhoenixStatement.Operation;
import org.apache.phoenix.optimize.Cost;
import org.apache.phoenix.parse.FilterableStatement;
import org.apache.phoenix.parse.OrderByNode;
import org.apache.phoenix.parse.JoinTableNode.JoinType;
import org.apache.phoenix.query.KeyRange;
import org.apache.phoenix.query.QueryServices;
import org.apache.phoenix.query.QueryServicesOptions;
import org.apache.phoenix.schema.ColumnFamilyNotFoundException;
import org.apache.phoenix.schema.ColumnNotFoundException;
import org.apache.phoenix.schema.KeyValueSchema;
import org.apache.phoenix.schema.KeyValueSchema.KeyValueSchemaBuilder;
import org.apache.phoenix.schema.PColumn;
import org.apache.phoenix.schema.PTable;
import org.apache.phoenix.schema.TableNotFoundException;
import org.apache.phoenix.schema.TableRef;
import org.apache.phoenix.schema.ValueBitSet;
import org.apache.phoenix.schema.tuple.Tuple;
import org.apache.phoenix.util.SchemaUtil;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
public class SortMergeJoinPlan implements QueryPlan {
private static final byte[] EMPTY_PTR = new byte[0];
private final StatementContext context;
private final FilterableStatement statement;
private final TableRef table;
private final JoinType type;
private final QueryPlan lhsPlan;
private final QueryPlan rhsPlan;
private final List<Expression> lhsKeyExpressions;
private final List<Expression> rhsKeyExpressions;
private final KeyValueSchema joinedSchema;
private final KeyValueSchema lhsSchema;
private final KeyValueSchema rhsSchema;
private final int rhsFieldPosition;
private final boolean isSingleValueOnly;
private final Set<TableRef> tableRefs;
private final long thresholdBytes;
private final boolean spoolingEnabled;
private Long estimatedBytes;
private Long estimatedRows;
private Long estimateInfoTs;
private boolean getEstimatesCalled;
private List<OrderBy> actualOutputOrderBys;
public SortMergeJoinPlan(
StatementContext context,
FilterableStatement statement,
TableRef table,
JoinType type,
QueryPlan lhsPlan,
QueryPlan rhsPlan,
Pair<List<Expression>,List<Expression>> lhsAndRhsKeyExpressions,
List<Expression> rhsKeyExpressions,
PTable joinedTable,
PTable lhsTable,
PTable rhsTable,
int rhsFieldPosition,
boolean isSingleValueOnly,
Pair<List<OrderByNode>,List<OrderByNode>> lhsAndRhsOrderByNodes) throws SQLException {
if (type == JoinType.Right) throw new IllegalArgumentException("JoinType should not be " + type);
this.context = context;
this.statement = statement;
this.table = table;
this.type = type;
this.lhsPlan = lhsPlan;
this.rhsPlan = rhsPlan;
this.lhsKeyExpressions = lhsAndRhsKeyExpressions.getFirst();
this.rhsKeyExpressions = lhsAndRhsKeyExpressions.getSecond();
this.joinedSchema = buildSchema(joinedTable);
this.lhsSchema = buildSchema(lhsTable);
this.rhsSchema = buildSchema(rhsTable);
this.rhsFieldPosition = rhsFieldPosition;
this.isSingleValueOnly = isSingleValueOnly;
this.tableRefs = Sets.newHashSetWithExpectedSize(lhsPlan.getSourceRefs().size() + rhsPlan.getSourceRefs().size());
this.tableRefs.addAll(lhsPlan.getSourceRefs());
this.tableRefs.addAll(rhsPlan.getSourceRefs());
this.thresholdBytes =
context.getConnection().getQueryServices().getProps().getLong(
QueryServices.CLIENT_SPOOL_THRESHOLD_BYTES_ATTRIB,
QueryServicesOptions.DEFAULT_CLIENT_SPOOL_THRESHOLD_BYTES);
this.spoolingEnabled =
context.getConnection().getQueryServices().getProps().getBoolean(
QueryServices.CLIENT_JOIN_SPOOLING_ENABLED_ATTRIB,
QueryServicesOptions.DEFAULT_CLIENT_JOIN_SPOOLING_ENABLED);
this.actualOutputOrderBys = convertActualOutputOrderBy(lhsAndRhsOrderByNodes.getFirst(), lhsAndRhsOrderByNodes.getSecond(), context);
}
@Override
public Operation getOperation() {
return statement.getOperation();
}
private static KeyValueSchema buildSchema(PTable table) {
KeyValueSchemaBuilder builder = new KeyValueSchemaBuilder(0);
if (table != null) {
for (PColumn column : table.getColumns()) {
if (!SchemaUtil.isPKColumn(column)) {
builder.addField(column);
}
}
}
return builder.build();
}
@Override
public ResultIterator iterator(ParallelScanGrouper scanGrouper) throws SQLException {
return iterator(scanGrouper, null);
}
@Override
public ResultIterator iterator(ParallelScanGrouper scanGrouper, Scan scan) throws SQLException {
return type == JoinType.Semi || type == JoinType.Anti ?
new SemiAntiJoinIterator(lhsPlan.iterator(scanGrouper), rhsPlan.iterator(scanGrouper)) :
new BasicJoinIterator(lhsPlan.iterator(scanGrouper), rhsPlan.iterator(scanGrouper));
}
@Override
public ResultIterator iterator() throws SQLException {
return iterator(DefaultParallelScanGrouper.getInstance());
}
@Override
public ExplainPlan getExplainPlan() throws SQLException {
List<String> steps = Lists.newArrayList();
steps.add("SORT-MERGE-JOIN (" + type.toString().toUpperCase() + ") TABLES");
for (String step : lhsPlan.getExplainPlan().getPlanSteps()) {
steps.add(" " + step);
}
steps.add("AND" + (rhsSchema.getFieldCount() == 0 ? " (SKIP MERGE)" : ""));
for (String step : rhsPlan.getExplainPlan().getPlanSteps()) {
steps.add(" " + step);
}
return new ExplainPlan(steps);
}
@Override
public Cost getCost() {
Double byteCount = this.accept(new ByteCountVisitor());
if (byteCount == null) {
return Cost.UNKNOWN;
}
Cost cost = new Cost(0, 0, byteCount);
return cost.plus(lhsPlan.getCost()).plus(rhsPlan.getCost());
}
@Override
public StatementContext getContext() {
return context;
}
@Override
public ParameterMetaData getParameterMetaData() {
return context.getBindManager().getParameterMetaData();
}
@Override
public long getEstimatedSize() {
return lhsPlan.getEstimatedSize() + rhsPlan.getEstimatedSize();
}
@Override
public TableRef getTableRef() {
return table;
}
@Override
public RowProjector getProjector() {
return null;
}
@Override
public Integer getLimit() {
return null;
}
@Override
public Integer getOffset() {
return null;
}
@Override
public OrderBy getOrderBy() {
return null;
}
@Override
public GroupBy getGroupBy() {
return null;
}
@Override
public List<KeyRange> getSplits() {
return Collections.<KeyRange> emptyList();
}
@Override
public List<List<Scan>> getScans() {
return Collections.<List<Scan>> emptyList();
}
@Override
public FilterableStatement getStatement() {
return statement;
}
@Override
public boolean isDegenerate() {
return false;
}
@Override
public boolean isRowKeyOrdered() {
return false;
}
public JoinType getJoinType() {
return type;
}
private static SQLException closeIterators(ResultIterator lhsIterator, ResultIterator rhsIterator) {
SQLException e = null;
try {
lhsIterator.close();
} catch (Throwable e1) {
e = e1 instanceof SQLException ? (SQLException)e1 : new SQLException(e1);
}
try {
rhsIterator.close();
} catch (Throwable e2) {
SQLException e22 = e2 instanceof SQLException ? (SQLException)e2 : new SQLException(e2);
if (e != null) {
e.setNextException(e22);
} else {
e = e22;
}
}
return e;
}
private class BasicJoinIterator implements ResultIterator {
private final ResultIterator lhsIterator;
private final ResultIterator rhsIterator;
private boolean initialized;
private Tuple lhsTuple;
private Tuple rhsTuple;
private JoinKey lhsKey;
private JoinKey rhsKey;
private Tuple nextLhsTuple;
private Tuple nextRhsTuple;
private JoinKey nextLhsKey;
private JoinKey nextRhsKey;
private ValueBitSet destBitSet;
private ValueBitSet lhsBitSet;
private ValueBitSet rhsBitSet;
private byte[] emptyProjectedValue;
private SizeAwareQueue<Tuple> queue;
private Iterator<Tuple> queueIterator;
public BasicJoinIterator(ResultIterator lhsIterator, ResultIterator rhsIterator) {
this.lhsIterator = lhsIterator;
this.rhsIterator = rhsIterator;
this.initialized = false;
this.lhsTuple = null;
this.rhsTuple = null;
this.lhsKey = new JoinKey(lhsKeyExpressions);
this.rhsKey = new JoinKey(rhsKeyExpressions);
this.nextLhsTuple = null;
this.nextRhsTuple = null;
this.nextLhsKey = new JoinKey(lhsKeyExpressions);
this.nextRhsKey = new JoinKey(rhsKeyExpressions);
this.destBitSet = ValueBitSet.newInstance(joinedSchema);
this.lhsBitSet = ValueBitSet.newInstance(lhsSchema);
this.rhsBitSet = ValueBitSet.newInstance(rhsSchema);
lhsBitSet.clear();
int len = lhsBitSet.getEstimatedLength();
this.emptyProjectedValue = new byte[len];
lhsBitSet.toBytes(emptyProjectedValue, 0);
this.queue = PhoenixQueues.newTupleQueue(spoolingEnabled, thresholdBytes);
this.queueIterator = null;
}
@Override
public void close() throws SQLException {
SQLException e = closeIterators(lhsIterator, rhsIterator);
try {
queue.close();
} catch (IOException t) {
if (e != null) {
e.setNextException(
new SQLException("Also encountered exception while closing queue", t));
} else {
e = new SQLException("Error while closing queue",t);
}
}
if (e != null) {
throw e;
}
}
@Override
public Tuple next() throws SQLException {
if (!initialized) {
init();
}
Tuple next = null;
while (next == null && !isEnd()) {
if (queueIterator != null) {
if (queueIterator.hasNext()) {
next = join(lhsTuple, queueIterator.next());
} else {
boolean eq = nextLhsTuple != null && lhsKey.equals(nextLhsKey);
advance(true);
if (eq) {
queueIterator = queue.iterator();
} else {
queue.clear();
queueIterator = null;
}
}
} else if (lhsTuple != null) {
if (rhsTuple != null) {
if (lhsKey.equals(rhsKey)) {
next = join(lhsTuple, rhsTuple);
if (nextLhsTuple != null && lhsKey.equals(nextLhsKey)) {
try {
queue.add(rhsTuple);
} catch (IllegalStateException e) {
throw new PhoenixIOException(e);
}
if (nextRhsTuple == null || !rhsKey.equals(nextRhsKey)) {
queueIterator = queue.iterator();
advance(true);
} else if (isSingleValueOnly) {
throw new SQLExceptionInfo.Builder(SQLExceptionCode.SINGLE_ROW_SUBQUERY_RETURNS_MULTIPLE_ROWS).build().buildException();
}
} else if (nextRhsTuple == null || !rhsKey.equals(nextRhsKey)) {
advance(true);
} else if (isSingleValueOnly) {
throw new SQLExceptionInfo.Builder(SQLExceptionCode.SINGLE_ROW_SUBQUERY_RETURNS_MULTIPLE_ROWS).build().buildException();
}
advance(false);
} else if (lhsKey.compareTo(rhsKey) < 0) {
if (type == JoinType.Full || type == JoinType.Left) {
next = join(lhsTuple, null);
}
advance(true);
} else {
if (type == JoinType.Full) {
next = join(null, rhsTuple);
}
advance(false);
}
} else { // left-join or full-join
next = join(lhsTuple, null);
advance(true);
}
} else { // full-join
next = join(null, rhsTuple);
advance(false);
}
}
return next;
}
@Override
public void explain(List<String> planSteps) {
}
private void init() throws SQLException {
nextLhsTuple = lhsIterator.next();
if (nextLhsTuple != null) {
nextLhsKey.evaluate(nextLhsTuple);
}
advance(true);
nextRhsTuple = rhsIterator.next();
if (nextRhsTuple != null) {
nextRhsKey.evaluate(nextRhsTuple);
}
advance(false);
initialized = true;
}
private void advance(boolean lhs) throws SQLException {
if (lhs) {
lhsTuple = nextLhsTuple;
lhsKey.set(nextLhsKey);
if (lhsTuple != null) {
nextLhsTuple = lhsIterator.next();
if (nextLhsTuple != null) {
nextLhsKey.evaluate(nextLhsTuple);
} else {
nextLhsKey.clear();
}
}
} else {
rhsTuple = nextRhsTuple;
rhsKey.set(nextRhsKey);
if (rhsTuple != null) {
nextRhsTuple = rhsIterator.next();
if (nextRhsTuple != null) {
nextRhsKey.evaluate(nextRhsTuple);
} else {
nextRhsKey.clear();
}
}
}
}
private boolean isEnd() {
return (lhsTuple == null && (rhsTuple == null || type != JoinType.Full))
|| (queueIterator == null && rhsTuple == null && type == JoinType.Inner);
}
private Tuple join(Tuple lhs, Tuple rhs) throws SQLException {
try {
ProjectedValueTuple t = null;
if (lhs == null) {
t = new ProjectedValueTuple(rhs, rhs.getValue(0).getTimestamp(),
this.emptyProjectedValue, 0, this.emptyProjectedValue.length,
this.emptyProjectedValue.length);
} else if (lhs instanceof ProjectedValueTuple) {
t = (ProjectedValueTuple) lhs;
} else {
ImmutableBytesWritable ptr = context.getTempPtr();
TupleProjector.decodeProjectedValue(lhs, ptr);
lhsBitSet.clear();
lhsBitSet.or(ptr);
int bitSetLen = lhsBitSet.getEstimatedLength();
t = new ProjectedValueTuple(lhs, lhs.getValue(0).getTimestamp(),
ptr.get(), ptr.getOffset(), ptr.getLength(), bitSetLen);
}
return rhsBitSet == ValueBitSet.EMPTY_VALUE_BITSET ?
t : TupleProjector.mergeProjectedValue(t, destBitSet,
rhs, rhsBitSet, rhsFieldPosition, true);
} catch (IOException e) {
throw new SQLException(e);
}
}
}
private class SemiAntiJoinIterator implements ResultIterator {
private final ResultIterator lhsIterator;
private final ResultIterator rhsIterator;
private final boolean isSemi;
private boolean initialized;
private Tuple lhsTuple;
private Tuple rhsTuple;
private JoinKey lhsKey;
private JoinKey rhsKey;
public SemiAntiJoinIterator(ResultIterator lhsIterator, ResultIterator rhsIterator) {
if (type != JoinType.Semi && type != JoinType.Anti) throw new IllegalArgumentException("Type " + type + " is not allowed by " + SemiAntiJoinIterator.class.getName());
this.lhsIterator = lhsIterator;
this.rhsIterator = rhsIterator;
this.isSemi = type == JoinType.Semi;
this.initialized = false;
this.lhsTuple = null;
this.rhsTuple = null;
this.lhsKey = new JoinKey(lhsKeyExpressions);
this.rhsKey = new JoinKey(rhsKeyExpressions);
}
@Override
public void close() throws SQLException {
SQLException e = closeIterators(lhsIterator, rhsIterator);
if (e != null) {
throw e;
}
}
@Override
public Tuple next() throws SQLException {
if (!initialized) {
advance(true);
advance(false);
initialized = true;
}
Tuple next = null;
while (lhsTuple != null && next == null) {
if (rhsTuple != null) {
if (lhsKey.equals(rhsKey)) {
if (isSemi) {
next = lhsTuple;
}
advance(true);
} else if (lhsKey.compareTo(rhsKey) < 0) {
if (!isSemi) {
next = lhsTuple;
}
advance(true);
} else {
advance(false);
}
} else {
if (!isSemi) {
next = lhsTuple;
}
advance(true);
}
}
return next;
}
@Override
public void explain(List<String> planSteps) {
}
private void advance(boolean lhs) throws SQLException {
if (lhs) {
lhsTuple = lhsIterator.next();
if (lhsTuple != null) {
lhsKey.evaluate(lhsTuple);
} else {
lhsKey.clear();
}
} else {
rhsTuple = rhsIterator.next();
if (rhsTuple != null) {
rhsKey.evaluate(rhsTuple);
} else {
rhsKey.clear();
}
}
}
}
private static class JoinKey implements Comparable<JoinKey> {
private final List<Expression> expressions;
private final List<ImmutableBytesWritable> keys;
public JoinKey(List<Expression> expressions) {
this.expressions = expressions;
this.keys = Lists.newArrayListWithExpectedSize(expressions.size());
for (int i = 0; i < expressions.size(); i++) {
this.keys.add(new ImmutableBytesWritable(EMPTY_PTR));
}
}
public void evaluate(Tuple tuple) {
for (int i = 0; i < keys.size(); i++) {
if (!expressions.get(i).evaluate(tuple, keys.get(i))) {
keys.get(i).set(EMPTY_PTR);
}
}
}
public void set(JoinKey other) {
for (int i = 0; i < keys.size(); i++) {
ImmutableBytesWritable key = other.keys.get(i);
this.keys.get(i).set(key.get(), key.getOffset(), key.getLength());
}
}
public void clear() {
for (int i = 0; i < keys.size(); i++) {
this.keys.get(i).set(EMPTY_PTR);
}
}
@Override
public boolean equals(Object other) {
if (!(other instanceof JoinKey))
return false;
return this.compareTo((JoinKey) other) == 0;
}
@Override
public int compareTo(JoinKey other) {
for (int i = 0; i < keys.size(); i++) {
int comp = this.keys.get(i).compareTo(other.keys.get(i));
if (comp != 0)
return comp;
}
return 0;
}
}
@Override
public boolean useRoundRobinIterator() {
return false;
}
@Override
public <T> T accept(QueryPlanVisitor<T> visitor) {
return visitor.visit(this);
}
@Override
public Set<TableRef> getSourceRefs() {
return tableRefs;
}
public QueryPlan getLhsPlan() {
return lhsPlan;
}
public QueryPlan getRhsPlan() {
return rhsPlan;
}
@Override
public Long getEstimatedRowsToScan() throws SQLException {
if (!getEstimatesCalled) {
getEstimates();
}
return estimatedRows;
}
@Override
public Long getEstimatedBytesToScan() throws SQLException {
if (!getEstimatesCalled) {
getEstimates();
}
return estimatedBytes;
}
@Override
public Long getEstimateInfoTimestamp() throws SQLException {
if (!getEstimatesCalled) {
getEstimates();
}
return estimateInfoTs;
}
private void getEstimates() throws SQLException {
getEstimatesCalled = true;
if ((lhsPlan.getEstimatedBytesToScan() == null || rhsPlan.getEstimatedBytesToScan() == null)
|| (lhsPlan.getEstimatedRowsToScan() == null
|| rhsPlan.getEstimatedRowsToScan() == null)
|| (lhsPlan.getEstimateInfoTimestamp() == null
|| rhsPlan.getEstimateInfoTimestamp() == null)) {
/*
* If any of the sub plans doesn't have the estimate info available, then we don't
* provide estimate for the overall plan
*/
estimatedBytes = null;
estimatedRows = null;
estimateInfoTs = null;
} else {
estimatedBytes =
add(add(estimatedBytes, lhsPlan.getEstimatedBytesToScan()),
rhsPlan.getEstimatedBytesToScan());
estimatedRows =
add(add(estimatedRows, lhsPlan.getEstimatedRowsToScan()),
rhsPlan.getEstimatedRowsToScan());
estimateInfoTs =
getMin(lhsPlan.getEstimateInfoTimestamp(), rhsPlan.getEstimateInfoTimestamp());
}
}
/**
* We do not use {@link #lhsKeyExpressions} and {@link #rhsKeyExpressions} directly because {@link #lhsKeyExpressions} is compiled by the
* {@link ColumnResolver} of lhs and {@link #rhsKeyExpressions} is compiled by the {@link ColumnResolver} of rhs, so we must recompile use
* the {@link ColumnResolver} of joinProjectedTables.
* @param lhsOrderByNodes
* @param rhsOrderByNodes
* @param statementContext
* @return
* @throws SQLException
*/
private static List<OrderBy> convertActualOutputOrderBy(
List<OrderByNode> lhsOrderByNodes,
List<OrderByNode> rhsOrderByNodes,
StatementContext statementContext) throws SQLException {
List<OrderBy> orderBys = new ArrayList<OrderBy>(2);
List<OrderByExpression> lhsOrderByExpressions =
compileOrderByNodes(lhsOrderByNodes, statementContext);
if(!lhsOrderByExpressions.isEmpty()) {
orderBys.add(new OrderBy(lhsOrderByExpressions));
}
List<OrderByExpression> rhsOrderByExpressions =
compileOrderByNodes(rhsOrderByNodes, statementContext);
if(!rhsOrderByExpressions.isEmpty()) {
orderBys.add(new OrderBy(rhsOrderByExpressions));
}
if(orderBys.isEmpty()) {
return Collections.<OrderBy> emptyList();
}
return orderBys;
}
private static List<OrderByExpression> compileOrderByNodes(List<OrderByNode> orderByNodes, StatementContext statementContext) throws SQLException {
/**
* If there is TableNotFoundException or ColumnNotFoundException, it means that the orderByNodes is not referenced by other parts of the sql,
* so could be ignored.
*/
StatelessExpressionCompiler expressionCompiler = new StatelessExpressionCompiler(statementContext);
List<OrderByExpression> orderByExpressions = new ArrayList<OrderByExpression>(orderByNodes.size());
for(OrderByNode orderByNode : orderByNodes) {
expressionCompiler.reset();
Expression expression = null;
try {
expression = orderByNode.getNode().accept(expressionCompiler);
} catch(TableNotFoundException exception) {
return orderByExpressions;
} catch(ColumnNotFoundException exception) {
return orderByExpressions;
} catch(ColumnFamilyNotFoundException exception) {
return orderByExpressions;
}
assert expression != null;
orderByExpressions.add(
OrderByExpression.createByCheckIfOrderByReverse(
expression,
orderByNode.isNullsLast(),
orderByNode.isAscending(),
false));
}
return orderByExpressions;
}
@Override
public List<OrderBy> getOutputOrderBys() {
return this.actualOutputOrderBys;
}
}