This is an automated email from the ASF dual-hosted git repository.
alexpl pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/ignite.git
The following commit(s) were added to refs/heads/master by this push:
new 7ae89601007 IGNITE-27661 SQL Calcite: Add non-equi condition support
for LEFT/RIGHT/FULL/ANTI HASH JOIN - Fixes #12669.
7ae89601007 is described below
commit 7ae8960100725d1fd006c108248279702260909e
Author: Aleksey Plekhanov <[email protected]>
AuthorDate: Mon Feb 2 09:39:56 2026 +0300
IGNITE-27661 SQL Calcite: Add non-equi condition support for
LEFT/RIGHT/FULL/ANTI HASH JOIN - Fixes #12669.
Signed-off-by: Aleksey Plekhanov <[email protected]>
---
.../query/calcite/exec/rel/HashJoinNode.java | 998 ++++++++++++---------
.../query/calcite/rule/HashJoinConverterRule.java | 12 +-
.../calcite/exec/rel/HashJoinExecutionTest.java | 330 ++++++-
.../calcite/integration/JoinIntegrationTest.java | 27 +-
.../query/calcite/planner/HashJoinPlannerTest.java | 34 +-
5 files changed, 914 insertions(+), 487 deletions(-)
diff --git
a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/rel/HashJoinNode.java
b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/rel/HashJoinNode.java
index fbdec244838..f845fb21d95 100644
---
a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/rel/HashJoinNode.java
+++
b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/rel/HashJoinNode.java
@@ -18,11 +18,12 @@
package org.apache.ignite.internal.processors.query.calcite.exec.rel;
import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Collections;
+import java.util.BitSet;
import java.util.HashMap;
import java.util.Iterator;
+import java.util.List;
import java.util.Map;
+import java.util.function.BiFunction;
import java.util.function.BiPredicate;
import org.apache.calcite.rel.core.JoinRelType;
import org.apache.calcite.rel.type.RelDataType;
@@ -33,78 +34,21 @@ import
org.apache.ignite.internal.processors.query.calcite.exec.RowHandler;
import
org.apache.ignite.internal.processors.query.calcite.exec.exp.agg.GroupKey;
import org.apache.ignite.internal.processors.query.calcite.rel.IgniteJoinInfo;
import
org.apache.ignite.internal.processors.query.calcite.type.IgniteTypeFactory;
-import org.apache.ignite.internal.util.typedef.F;
import org.jetbrains.annotations.Nullable;
/** Hash join implementor. */
public abstract class HashJoinNode<Row> extends
AbstractRightMaterializedJoinNode<Row> {
- /** */
- private static final int INITIAL_CAPACITY = 128;
-
- /** */
- private final RowHandler<Row> leftRowHnd;
-
- /** */
- private final RowHandler<Row> rightRowHnd;
-
- /** */
- private final boolean keepRowsWithNull;
-
- /** */
- private final ImmutableBitSet allowNulls;
-
- /** Output row handler. */
- protected final RowHandler<Row> outRowHnd;
-
- /** Right rows storage. */
- protected Map<GroupKey<Row>, TouchedArrayList<Row>> hashStore = new
HashMap<>(INITIAL_CAPACITY);
-
- /** */
- protected Iterator<Row> rightIt = Collections.emptyIterator();
-
- /** */
- @Nullable protected final BiPredicate<Row, Row> nonEqCond;
-
/**
* Creates hash join node.
*
* @param ctx Execution context.
* @param rowType Out row type.
- * @param info Join info.
- * @param outRowHnd Output row handler.
- * @param keepRowsWithNull {@code True} if we need to store the row from
right shoulder even if it contains NULL in
- * any of join key position. This is required for
joins which emit unmatched part
- * of the right shoulder, such as RIGHT JOIN and
FULL OUTER JOIN.
- * @param nonEqCond If provided, only rows matching the predicate will be
emitted as matched rows.
*/
protected HashJoinNode(
ExecutionContext<Row> ctx,
- RelDataType rowType,
- IgniteJoinInfo info,
- RowHandler<Row> outRowHnd,
- boolean keepRowsWithNull,
- @Nullable BiPredicate<Row, Row> nonEqCond
+ RelDataType rowType
) {
super(ctx, rowType);
-
- allowNulls = info.allowNulls();
- this.keepRowsWithNull = keepRowsWithNull;
-
- leftRowHnd = new MappingRowHandler<>(ctx.rowHandler(),
info.leftKeys.toIntArray());
- rightRowHnd = new MappingRowHandler<>(ctx.rowHandler(),
info.rightKeys.toIntArray());
-
- this.outRowHnd = outRowHnd;
-
- this.nonEqCond = nonEqCond;
- }
-
- /** {@inheritDoc} */
- @Override protected void rewindInternal() {
- super.rewindInternal();
-
- rightIt = Collections.emptyIterator();
-
- hashStore.clear();
}
/** Creates certain join node. */
@@ -117,8 +61,7 @@ public abstract class HashJoinNode<Row> extends
AbstractRightMaterializedJoinNod
IgniteJoinInfo info,
@Nullable BiPredicate<RowT, RowT> nonEqCond
) {
- assert !info.pairs().isEmpty() && (info.isEqui() || type ==
JoinRelType.INNER || type == JoinRelType.SEMI);
- assert nonEqCond == null || type == JoinRelType.INNER || type ==
JoinRelType.SEMI;
+ assert !info.pairs().isEmpty();
IgniteTypeFactory typeFactory = ctx.getTypeFactory();
RowHandler<RowT> rowHnd = ctx.rowHandler();
@@ -128,21 +71,34 @@ public abstract class HashJoinNode<Row> extends
AbstractRightMaterializedJoinNod
return new InnerHashJoin<>(ctx, rowType, info, rowHnd,
nonEqCond);
case LEFT:
- return new LeftHashJoin<>(ctx, rowType, info, rowHnd,
rowHnd.factory(typeFactory, rightRowType));
+ return new LeftHashJoin<>(ctx, rowType, info, rowHnd,
rowHnd.factory(typeFactory, rightRowType),
+ nonEqCond);
case RIGHT:
- return new RightHashJoin<>(ctx, rowType, info, rowHnd,
rowHnd.factory(typeFactory, leftRowType));
+ if (nonEqCond == null) {
+ return new RightHashJoin<>(ctx, rowType, info, rowHnd,
+ rowHnd.factory(typeFactory, leftRowType), nonEqCond);
+ }
+ else {
+ return new RightRowTouchingHashJoin<>(ctx, rowType, info,
rowHnd,
+ rowHnd.factory(typeFactory, leftRowType), nonEqCond);
+ }
- case FULL: {
- return new FullOuterHashJoin<>(ctx, rowType, info, rowHnd,
rowHnd.factory(typeFactory, leftRowType),
- rowHnd.factory(typeFactory, rightRowType), nonEqCond);
- }
+ case FULL:
+ if (nonEqCond == null) {
+ return new FullOuterHashJoin<>(ctx, rowType, info, rowHnd,
+ rowHnd.factory(typeFactory, leftRowType),
rowHnd.factory(typeFactory, rightRowType), nonEqCond);
+ }
+ else {
+ return new FullOuterRowTouchingHashJoin<>(ctx, rowType,
info, rowHnd,
+ rowHnd.factory(typeFactory, leftRowType),
rowHnd.factory(typeFactory, rightRowType), nonEqCond);
+ }
case SEMI:
- return new SemiHashJoin<>(ctx, rowType, info, rowHnd,
nonEqCond);
+ return new SemiHashJoin<>(ctx, rowType, info, nonEqCond);
case ANTI:
- return new AntiHashJoin<>(ctx, rowType, info, rowHnd,
nonEqCond);
+ return new AntiHashJoin<>(ctx, rowType, info, nonEqCond);
default:
throw new IllegalArgumentException("Join of type '" + type +
"' isn't supported.");
@@ -150,251 +106,210 @@ public abstract class HashJoinNode<Row> extends
AbstractRightMaterializedJoinNod
}
/** */
- protected Collection<Row> lookup(Row row) {
- GroupKey<Row> key = GroupKey.of(row, leftRowHnd, allowNulls);
-
- if (key == null)
- return Collections.emptyList();
-
- TouchedArrayList<Row> res = hashStore.get(key);
-
- if (res == null)
- return Collections.emptyList();
-
- res.touched = true;
-
- return res;
- }
-
- /** */
- protected Iterator<Row> untouched() {
- return F.flat(F.iterator(hashStore.values(), c0 -> c0, true, c1 ->
!c1.touched));
- }
-
- /** {@inheritDoc} */
- @Override protected void pushRight(Row row) throws Exception {
- assert downstream() != null;
- assert waitingRight > 0;
-
- waitingRight--;
-
- GroupKey<Row> key = keepRowsWithNull ? GroupKey.of(row, rightRowHnd) :
GroupKey.of(row, rightRowHnd, allowNulls);
-
- if (key != null) {
- nodeMemoryTracker.onRowAdded(row);
-
- hashStore.computeIfAbsent(key, k -> new
TouchedArrayList<>()).add(row);
- }
+ private abstract static class AbstractStoringHashJoin<Row, RowList extends
List<Row>> extends HashJoinNode<Row> {
+ /** */
+ private static final int INITIAL_CAPACITY = 128;
- if (waitingRight == 0)
- rightSource().request(waitingRight = IN_BUFFER_SIZE);
- }
+ /** */
+ private final RowHandler<Row> leftRowHnd;
- /** */
- protected boolean leftFinished() {
- return waitingLeft == NOT_WAITING && left == null &&
leftInBuf.isEmpty();
- }
+ /** */
+ private final RowHandler<Row> rightRowHnd;
- /** */
- protected boolean rightFinished() {
- return waitingRight == NOT_WAITING && !rightIt.hasNext();
- }
+ /** */
+ private final boolean keepRowsWithNull;
- /** */
- protected boolean checkJoinFinished() throws Exception {
- if (requested > 0 && leftFinished() && rightFinished()) {
- requested = 0;
+ /** */
+ private final ImmutableBitSet allowNulls;
- hashStore.clear();
+ /** */
+ protected @Nullable RowList rightRows;
- downstream().end();
+ /** */
+ protected int rightIdx;
- return true;
- }
+ /** */
+ @Nullable protected final BiPredicate<Row, Row> nonEqCond;
- return false;
- }
+ /** Right rows storage. */
+ protected Map<GroupKey<Row>, RowList> hashStore = new
HashMap<>(INITIAL_CAPACITY);
- /** */
- private static final class InnerHashJoin<RowT> extends HashJoinNode<RowT> {
/**
- * Creates node for INNER JOIN.
+ * Constructor.
*
* @param ctx Execution context.
* @param rowType Out row type.
* @param info Join info.
- * @param outRowHnd Output row handler.
+ * @param keepRowsWithNull {@code True} if we need to store the row
from right hand even if it contains NULL in
+ * any of join key position. This is required
for joins which emit unmatched part
+ * of the right hand, such as RIGHT JOIN and
FULL OUTER JOIN.
* @param nonEqCond If provided, only rows matching the predicate will
be emitted as matched rows.
*/
- private InnerHashJoin(ExecutionContext<RowT> ctx,
+ protected AbstractStoringHashJoin(
+ ExecutionContext<Row> ctx,
RelDataType rowType,
IgniteJoinInfo info,
- RowHandler<RowT> outRowHnd,
- @Nullable BiPredicate<RowT, RowT> nonEqCond
+ boolean keepRowsWithNull,
+ @Nullable BiPredicate<Row, Row> nonEqCond
) {
- super(ctx, rowType, info, outRowHnd, false, nonEqCond);
+ super(ctx, rowType);
+
+ allowNulls = info.allowNulls();
+ this.keepRowsWithNull = keepRowsWithNull;
+
+ leftRowHnd = new MappingRowHandler<>(ctx.rowHandler(),
info.leftKeys.toIntArray());
+ rightRowHnd = new MappingRowHandler<>(ctx.rowHandler(),
info.rightKeys.toIntArray());
+
+ this.nonEqCond = nonEqCond;
}
/** {@inheritDoc} */
- @Override protected void join() throws Exception {
- if (waitingRight == NOT_WAITING) {
- inLoop = true;
+ @Override protected void rewindInternal() {
+ super.rewindInternal();
- try {
- while (requested > 0 && (left != null ||
!leftInBuf.isEmpty())) {
- // Proceed with next left row, if previous was fully
processed.
- if (left == null) {
- left = leftInBuf.remove();
+ rightRows = null;
+ rightIdx = 0;
- rightIt = lookup(left).iterator();
- }
+ hashStore.clear();
+ }
- if (rightIt.hasNext()) {
- // Emits matched rows.
- while (requested > 0 && rightIt.hasNext()) {
- if (rescheduleJoin())
- return;
+ /** */
+ protected @Nullable RowList lookup(Row row) {
+ GroupKey<Row> key = GroupKey.of(row, leftRowHnd, allowNulls);
- RowT right = rightIt.next();
+ if (key == null)
+ return null;
- if (nonEqCond != null && !nonEqCond.test(left,
right))
- continue;
+ return hashStore.get(key);
+ }
- --requested;
+ /** {@inheritDoc} */
+ @Override protected void pushRight(Row row) throws Exception {
+ assert downstream() != null;
+ assert waitingRight > 0;
- downstream().push(outRowHnd.concat(left,
right));
- }
+ waitingRight--;
- if (!rightIt.hasNext())
- left = null;
- }
- else
- left = null;
- }
- }
- finally {
- inLoop = false;
- }
- }
+ GroupKey<Row> key = keepRowsWithNull ? GroupKey.of(row,
rightRowHnd) : GroupKey.of(row, rightRowHnd, allowNulls);
- if (checkJoinFinished())
- return;
-
- tryToRequestInputs();
- }
- }
+ if (key != null) {
+ nodeMemoryTracker.onRowAdded(row);
- /** */
- private static final class LeftHashJoin<RowT> extends HashJoinNode<RowT> {
- /** Right row factory. */
- private final RowHandler.RowFactory<RowT> rightRowFactory;
+ hashStore.computeIfAbsent(key, k -> createRowList()).add(row);
+ }
- /**
- * Creates node for LEFT OUTER JOIN.
- *
- * @param ctx Execution context.
- * @param info Join info.
- * @param rowType Out row type.
- * @param outRowHnd Output row handler.
- * @param rightRowFactory Right row factory.
- */
- private LeftHashJoin(
- ExecutionContext<RowT> ctx,
- RelDataType rowType,
- IgniteJoinInfo info,
- RowHandler<RowT> outRowHnd,
- RowHandler.RowFactory<RowT> rightRowFactory
- ) {
- super(ctx, rowType, info, outRowHnd, false, null);
+ if (waitingRight == 0)
+ rightSource().request(waitingRight = IN_BUFFER_SIZE);
+ }
- assert nonEqCond == null : "Non equi condition is not supported in
LEFT join";
+ /** */
+ protected abstract RowList createRowList();
- this.rightRowFactory = rightRowFactory;
+ /** */
+ protected boolean hasNextRight() {
+ return rightRows != null && rightIdx < rightRows.size();
}
- /** {@inheritDoc} */
- @Override protected void join() throws Exception {
- if (waitingRight == NOT_WAITING) {
- inLoop = true;
+ /** */
+ protected Row nextRight() {
+ return rightRows.get(rightIdx++);
+ }
- try {
- while (requested > 0 && (left != null ||
!leftInBuf.isEmpty())) {
- // Proceed with next left row, if previous was fully
processed.
- if (left == null) {
- left = leftInBuf.remove();
+ /** */
+ protected boolean leftFinished() {
+ return waitingLeft == NOT_WAITING && left == null &&
leftInBuf.isEmpty();
+ }
- Collection<RowT> rightRows = lookup(left);
+ /** */
+ protected boolean rightFinished() {
+ return waitingRight == NOT_WAITING && !hasNextRight();
+ }
- // Emit unmatched left row.
- if (rightRows.isEmpty()) {
- requested--;
+ /** */
+ protected boolean checkNextLeftRowStarted() {
+ // Proceed with next left row, if previous was fully processed.
+ if (left == null) {
+ left = leftInBuf.remove();
- downstream().push(outRowHnd.concat(left,
rightRowFactory.create()));
- }
+ rightRows = lookup(left);
+ rightIdx = 0;
- rightIt = rightRows.iterator();
- }
+ return true;
+ }
- if (rightIt.hasNext()) {
- while (requested > 0 && rightIt.hasNext()) {
- if (rescheduleJoin())
- return;
+ return false;
+ }
- RowT right = rightIt.next();
+ /** */
+ protected boolean checkJoinFinished() throws Exception {
+ if (requested > 0 && leftFinished() && rightFinished()) {
+ requested = 0;
- --requested;
+ hashStore.clear();
- downstream().push(outRowHnd.concat(left,
right));
- }
+ downstream().end();
- if (!rightIt.hasNext())
- left = null;
- }
- else
- left = null;
- }
- }
- finally {
- inLoop = false;
- }
+ return true;
}
- if (checkJoinFinished())
- return;
-
- tryToRequestInputs();
+ return false;
}
}
/** */
- private static final class RightHashJoin<RowT> extends HashJoinNode<RowT> {
- /** Left row factory. */
- private final RowHandler.RowFactory<RowT> leftRowFactory;
+ private abstract static class AbstractMatchingHashJoin<Row, RowList
extends List<Row>>
+ extends AbstractStoringHashJoin<Row, RowList> {
+ /** Output row factory. */
+ private final BiFunction<Row, Row, Row> outRowFactory;
+
+ /** Empty right row. */
+ private final @Nullable Row emptyRightRow;
+
+ /** Empty left row. */
+ private final @Nullable Row emptyLeftRow;
+
+ /** Whether current left row was matched. */
+ protected boolean leftMatched;
/** */
- private boolean drainMaterialization;
+ protected boolean drainMaterialization;
+
+ /** */
+ protected Iterator<RowList> materializedIt;
/**
- * Creates node for RIGHT OUTER JOIN.
+ * Constructor.
*
* @param ctx Execution context.
- * @param rowType Out row type.
+ * @param rowType Row type.
* @param info Join info.
- * @param outRowHnd Output row handler.
- * @param leftRowFactory Left row factory.
+ * @param outRowHnd Out row handler.
+ * @param leftRowFactory Left row factory (or {@code null} if join
don't produce rows for unmatched right side).
+ * @param rightRowFactory Right row factory (or {@code null} if join
don't produce rows for unmatched left side).
*/
- private RightHashJoin(
- ExecutionContext<RowT> ctx,
+ private AbstractMatchingHashJoin(
+ ExecutionContext<Row> ctx,
RelDataType rowType,
IgniteJoinInfo info,
- RowHandler<RowT> outRowHnd,
- RowHandler.RowFactory<RowT> leftRowFactory
+ RowHandler<Row> outRowHnd,
+ @Nullable RowHandler.RowFactory<Row> leftRowFactory,
+ @Nullable RowHandler.RowFactory<Row> rightRowFactory,
+ @Nullable BiPredicate<Row, Row> nonEqCond
) {
- super(ctx, rowType, info, outRowHnd, true, null);
+ super(ctx, rowType, info, leftRowFactory != null, nonEqCond);
+
+ outRowFactory = outRowHnd::concat;
+ emptyLeftRow = leftRowFactory == null ? null :
leftRowFactory.create();
+ emptyRightRow = rightRowFactory == null ? null :
rightRowFactory.create();
+ }
- assert nonEqCond == null : "Non equi condition is not supported in
RIGHT join";
+ /** {@inheritDoc} */
+ @Override protected void rewindInternal() {
+ super.rewindInternal();
- this.leftRowFactory = leftRowFactory;
+ drainMaterialization = false;
+
+ leftMatched = false;
}
/** {@inheritDoc} */
@@ -404,63 +319,61 @@ public abstract class HashJoinNode<Row> extends
AbstractRightMaterializedJoinNod
try {
while (requested > 0 && (left != null ||
!leftInBuf.isEmpty())) {
- // Proceed with next left row, if previous was fully
processed.
- if (left == null) {
- left = leftInBuf.remove();
+ if (checkNextLeftRowStarted())
+ leftMatched = false;
- rightIt = lookup(left).iterator();
- }
+ while (hasNextRight()) {
+ if (rescheduleJoin())
+ return;
- if (rightIt.hasNext()) {
- // Emits matched rows.
- while (requested > 0 && rightIt.hasNext()) {
- if (rescheduleJoin())
- return;
+ Row right = nextRight();
- RowT right = rightIt.next();
+ if (nonEqCond != null && !nonEqCond.test(left,
right))
+ continue;
- --requested;
+ leftMatched = true;
- downstream().push(outRowHnd.concat(left,
right));
- }
+ // Emit matched row.
+ downstreamPush(left, right);
+
+ touchRight();
- if (!rightIt.hasNext())
- left = null;
+ if (requested == 0)
+ return;
}
- else
- left = null;
- }
- }
- finally {
- inLoop = false;
- }
- }
- // Emit unmatched right rows.
- if (leftFinished() && waitingRight == NOT_WAITING && requested >
0) {
- inLoop = true;
+ assert requested > 0;
- try {
- if (!rightIt.hasNext() && !drainMaterialization) {
- // Prevent scanning store more than once.
- drainMaterialization = true;
+ // For LEFT/FULL join.
+ if (emptyRightRow != null && !leftMatched) {
+ // Emit unmatched left row.
+ downstreamPush(left, emptyRightRow);
+ }
- rightIt = untouched();
+ left = null;
}
- RowT emptyLeft = leftRowFactory.create();
+ // For RIGHT/FULL join.
+ if (emptyLeftRow != null && leftFinished() && requested >
0) {
+ // Emit unmatched right rows.
+ if (!hasNextRight() && !drainMaterialization) {
+ // Prevent scanning store more than once.
+ drainMaterialization = true;
- while (requested > 0 && rightIt.hasNext()) {
- if (rescheduleJoin())
- return;
+ materializedIt = hashStore.values().iterator();
+ }
- RowT right = rightIt.next();
+ while (requested > 0 && hasNextRight()) {
+ if (rescheduleJoin())
+ return;
- RowT row = outRowHnd.concat(emptyLeft, right);
+ Row right = nextRight();
- --requested;
+ if (touchedRight())
+ continue;
- downstream().push(row);
+ downstreamPush(emptyLeftRow, right);
+ }
}
}
finally {
@@ -474,27 +387,42 @@ public abstract class HashJoinNode<Row> extends
AbstractRightMaterializedJoinNod
tryToRequestInputs();
}
- /** {@inheritDoc} */
- @Override protected void rewindInternal() {
- drainMaterialization = false;
+ /** */
+ private void downstreamPush(Row left, Row right) throws Exception {
+ requested--;
- super.rewindInternal();
+ downstream().push(outRowFactory.apply(left, right));;
}
- }
- /** */
- private static final class FullOuterHashJoin<RowT> extends
HashJoinNode<RowT> {
- /** Left row factory. */
- private final RowHandler.RowFactory<RowT> leftRowFactory;
+ /** {@inheritDoc} */
+ @Override protected boolean hasNextRight() {
+ boolean res = super.hasNextRight();
+
+ if (drainMaterialization && !res && materializedIt.hasNext()) {
+ rightRows = materializedIt.next();
+ rightIdx = 0;
+ return true; // Every key in hashStore contains at least one
row.
+ }
+
+ return res;
+ }
- /** Right row factory. */
- private final RowHandler.RowFactory<RowT> rightRowFactory;
+ /** */
+ protected void touchRight() {
+ // No-op.
+ }
/** */
- private boolean drainMaterialization;
+ protected boolean touchedRight() {
+ return false;
+ }
+ }
+ /** */
+ private abstract static class AbstractNoTouchingHashJoin<RowT>
+ extends AbstractMatchingHashJoin<RowT, ArrayList<RowT>> {
/**
- * Creates node for FULL OUTER JOIN.
+ * Constructor.
*
* @param ctx Execution context.
* @param rowType Row type.
@@ -502,135 +430,306 @@ public abstract class HashJoinNode<Row> extends
AbstractRightMaterializedJoinNod
* @param outRowHnd Out row handler.
* @param leftRowFactory Left row factory.
* @param rightRowFactory Right row factory.
+ * @param nonEqCond If provided, only rows matching the predicate will
be emitted as matched rows.
*/
- private FullOuterHashJoin(
+ private AbstractNoTouchingHashJoin(
ExecutionContext<RowT> ctx,
RelDataType rowType,
IgniteJoinInfo info,
RowHandler<RowT> outRowHnd,
- RowHandler.RowFactory<RowT> leftRowFactory,
- RowHandler.RowFactory<RowT> rightRowFactory,
+ @Nullable RowHandler.RowFactory<RowT> leftRowFactory,
+ @Nullable RowHandler.RowFactory<RowT> rightRowFactory,
@Nullable BiPredicate<RowT, RowT> nonEqCond
) {
- super(ctx, rowType, info, outRowHnd, true, null);
-
- assert nonEqCond == null : "Non equi condition is not supported in
FULL OUTER join";
-
- this.leftRowFactory = leftRowFactory;
- this.rightRowFactory = rightRowFactory;
+ super(ctx, rowType, info, outRowHnd, leftRowFactory,
rightRowFactory, nonEqCond);
}
/** {@inheritDoc} */
- @Override protected void join() throws Exception {
- if (waitingRight == NOT_WAITING) {
- inLoop = true;
-
- try {
- while (requested > 0 && (left != null ||
!leftInBuf.isEmpty())) {
- // Proceed with next left row, if previous was fully
processed.
- if (left == null) {
- left = leftInBuf.remove();
-
- Collection<RowT> rightRows = lookup(left);
+ @Override protected ArrayList<RowT> createRowList() {
+ return new ArrayList<>();
+ }
+ }
- if (rightRows.isEmpty()) {
- // Emit empty right row for unmatched left row.
- rightIt =
Collections.singletonList(rightRowFactory.create()).iterator();
- }
- else
- rightIt = rightRows.iterator();
- }
+ /** */
+ private static final class InnerHashJoin<RowT> extends
AbstractNoTouchingHashJoin<RowT> {
+ /**
+ * Creates node for INNER JOIN.
+ *
+ * @param ctx Execution context.
+ * @param rowType Out row type.
+ * @param info Join info.
+ * @param outRowHnd Output row handler.
+ * @param nonEqCond If provided, only rows matching the predicate will
be emitted as matched rows.
+ */
+ private InnerHashJoin(
+ ExecutionContext<RowT> ctx,
+ RelDataType rowType,
+ IgniteJoinInfo info,
+ RowHandler<RowT> outRowHnd,
+ @Nullable BiPredicate<RowT, RowT> nonEqCond
+ ) {
+ super(ctx, rowType, info, outRowHnd, null, null, nonEqCond);
+ }
+ }
- if (rightIt.hasNext()) {
- // Emits matched rows.
- while (requested > 0 && rightIt.hasNext()) {
- if (rescheduleJoin())
- return;
+ /** */
+ private static final class LeftHashJoin<RowT> extends
AbstractNoTouchingHashJoin<RowT> {
+ /**
+ * Creates node for LEFT OUTER JOIN.
+ *
+ * @param ctx Execution context.
+ * @param info Join info.
+ * @param rowType Out row type.
+ * @param outRowHnd Output row handler.
+ * @param rightRowFactory Right row factory.
+ * @param nonEqCond If provided, only rows matching the predicate will
be emitted as matched rows.
+ */
+ private LeftHashJoin(
+ ExecutionContext<RowT> ctx,
+ RelDataType rowType,
+ IgniteJoinInfo info,
+ RowHandler<RowT> outRowHnd,
+ RowHandler.RowFactory<RowT> rightRowFactory,
+ @Nullable BiPredicate<RowT, RowT> nonEqCond
+ ) {
+ super(ctx, rowType, info, outRowHnd, null, rightRowFactory,
nonEqCond);
+ }
+ }
- RowT right = rightIt.next();
+ /** */
+ private abstract static class AbstractListTouchingHashJoin<RowT>
+ extends AbstractMatchingHashJoin<RowT, TouchableList<RowT>> {
+ /**
+ * Constructor.
+ *
+ * @param ctx Execution context.
+ * @param rowType Row type.
+ * @param info Join info.
+ * @param outRowHnd Out row handler.
+ * @param leftRowFactory Left row factory.
+ * @param rightRowFactory Right row factory.
+ * @param nonEqCond If provided, only rows matching the predicate will
be emitted as matched rows.
+ */
+ private AbstractListTouchingHashJoin(
+ ExecutionContext<RowT> ctx,
+ RelDataType rowType,
+ IgniteJoinInfo info,
+ RowHandler<RowT> outRowHnd,
+ @Nullable RowHandler.RowFactory<RowT> leftRowFactory,
+ @Nullable RowHandler.RowFactory<RowT> rightRowFactory,
+ @Nullable BiPredicate<RowT, RowT> nonEqCond
+ ) {
+ super(ctx, rowType, info, outRowHnd, leftRowFactory,
rightRowFactory, nonEqCond);
- --requested;
+ assert nonEqCond == null;
+ }
- downstream().push(outRowHnd.concat(left,
right));
- }
+ /** {@inheritDoc} */
+ @Override protected TouchableList<RowT> createRowList() {
+ return new TouchableList<>();
+ }
- if (!rightIt.hasNext())
- left = null;
- }
- else
- left = null;
- }
- }
- finally {
- inLoop = false;
- }
- }
+ /** {@inheritDoc} */
+ @Override protected @Nullable TouchableList<RowT> lookup(RowT t) {
+ TouchableList<RowT> res = super.lookup(t);
- // Emit unmatched right rows.
- if (leftFinished() && waitingRight == NOT_WAITING && requested >
0) {
- inLoop = true;
+ if (res != null)
+ res.touch();
- try {
- if (!rightIt.hasNext() && !drainMaterialization) {
- // Prevent scanning store more than once.
- drainMaterialization = true;
+ return res;
+ }
- rightIt = untouched();
- }
+ /** {@inheritDoc} */
+ @Override protected boolean touchedRight() {
+ return rightRows.touched();
+ }
- RowT emptyLeft = leftRowFactory.create();
+ /** {@inheritDoc} */
+ @Override protected boolean hasNextRight() {
+ boolean res = super.hasNextRight();
+
+ if (drainMaterialization && res && rightIdx > 0 &&
rightRows.touched()) {
+ // Emit one row even for touched list, to correctly reschedule
after some rows have been processed
+ // and allow others to do their job.
+ if (materializedIt.hasNext()) {
+ rightRows = materializedIt.next();
+ rightIdx = 0;
+ return true; // Every key in hashStore contains at least
one row.
+ }
- while (requested > 0 && rightIt.hasNext()) {
- if (rescheduleJoin())
- return;
+ return false;
+ }
- RowT right = rightIt.next();
+ return res;
+ }
+ }
- RowT row = outRowHnd.concat(emptyLeft, right);
+ /** */
+ private static class RightHashJoin<RowT> extends
AbstractListTouchingHashJoin<RowT> {
+ /**
+ * Creates node for RIGHT OUTER JOIN.
+ *
+ * @param ctx Execution context.
+ * @param rowType Out row type.
+ * @param info Join info.
+ * @param outRowHnd Output row handler.
+ * @param leftRowFactory Left row factory.
+ * @param nonEqCond If provided, only rows matching the predicate will
be emitted as matched rows.
+ */
+ private RightHashJoin(
+ ExecutionContext<RowT> ctx,
+ RelDataType rowType,
+ IgniteJoinInfo info,
+ RowHandler<RowT> outRowHnd,
+ RowHandler.RowFactory<RowT> leftRowFactory,
+ @Nullable BiPredicate<RowT, RowT> nonEqCond
+ ) {
+ super(ctx, rowType, info, outRowHnd, leftRowFactory, null,
nonEqCond);
+ }
+ }
- --requested;
+ /** */
+ private static final class FullOuterHashJoin<RowT> extends
AbstractListTouchingHashJoin<RowT> {
+ /**
+ * Creates node for FULL OUTER JOIN.
+ *
+ * @param ctx Execution context.
+ * @param rowType Row type.
+ * @param info Join info.
+ * @param outRowHnd Out row handler.
+ * @param leftRowFactory Left row factory.
+ * @param rightRowFactory Right row factory.
+ * @param nonEqCond If provided, only rows matching the predicate will
be emitted as matched rows.
+ */
+ private FullOuterHashJoin(
+ ExecutionContext<RowT> ctx,
+ RelDataType rowType,
+ IgniteJoinInfo info,
+ RowHandler<RowT> outRowHnd,
+ RowHandler.RowFactory<RowT> leftRowFactory,
+ RowHandler.RowFactory<RowT> rightRowFactory,
+ @Nullable BiPredicate<RowT, RowT> nonEqCond
+ ) {
+ super(ctx, rowType, info, outRowHnd, leftRowFactory,
rightRowFactory, nonEqCond);
+ }
+ }
- downstream().push(row);
- }
- }
- finally {
- inLoop = false;
- }
- }
+ /** */
+ private abstract static class AbstractRowTouchingHashJoin<RowT>
+ extends AbstractMatchingHashJoin<RowT, RowTouchableList<RowT>> {
+ /**
+ * Constructor.
+ *
+ * @param ctx Execution context.
+ * @param rowType Row type.
+ * @param info Join info.
+ * @param outRowHnd Out row handler.
+ * @param leftRowFactory Left row factory.
+ * @param rightRowFactory Right row factory.
+ * @param nonEqCond If provided, only rows matching the predicate will
be emitted as matched rows.
+ */
+ private AbstractRowTouchingHashJoin(
+ ExecutionContext<RowT> ctx,
+ RelDataType rowType,
+ IgniteJoinInfo info,
+ RowHandler<RowT> outRowHnd,
+ @Nullable RowHandler.RowFactory<RowT> leftRowFactory,
+ @Nullable RowHandler.RowFactory<RowT> rightRowFactory,
+ @Nullable BiPredicate<RowT, RowT> nonEqCond
+ ) {
+ super(ctx, rowType, info, outRowHnd, leftRowFactory,
rightRowFactory, nonEqCond);
- if (checkJoinFinished())
- return;
+ assert nonEqCond != null;
+ }
- tryToRequestInputs();
+ /** {@inheritDoc} */
+ @Override protected RowTouchableList<RowT> createRowList() {
+ return new RowTouchableList<>();
}
/** {@inheritDoc} */
- @Override protected void rewindInternal() {
- drainMaterialization = false;
+ @Override protected void touchRight() {
+ // Index was already moved by nextRight call, so use previous
index.
+ rightRows.touch(rightIdx - 1);
+ }
- super.rewindInternal();
+ /** {@inheritDoc} */
+ @Override protected boolean touchedRight() {
+ // Index was already moved by nextRight call, so use previous
index.
+ return rightRows.touched(rightIdx - 1);
}
}
/** */
- private static final class SemiHashJoin<RowT> extends HashJoinNode<RowT> {
+ private static class RightRowTouchingHashJoin<RowT> extends
AbstractRowTouchingHashJoin<RowT> {
/**
- * Creates node for SEMI JOIN operator.
+ * Creates node for RIGHT OUTER JOIN.
*
* @param ctx Execution context.
* @param rowType Out row type.
* @param info Join info.
* @param outRowHnd Output row handler.
+ * @param leftRowFactory Left row factory.
* @param nonEqCond If provided, only rows matching the predicate will
be emitted as matched rows.
*/
- private SemiHashJoin(
+ private RightRowTouchingHashJoin(
ExecutionContext<RowT> ctx,
RelDataType rowType,
IgniteJoinInfo info,
RowHandler<RowT> outRowHnd,
+ RowHandler.RowFactory<RowT> leftRowFactory,
@Nullable BiPredicate<RowT, RowT> nonEqCond
) {
- super(ctx, rowType, info, outRowHnd, false, nonEqCond);
+ super(ctx, rowType, info, outRowHnd, leftRowFactory, null,
nonEqCond);
+ }
+ }
+
+ /** */
+ private static final class FullOuterRowTouchingHashJoin<RowT> extends
AbstractRowTouchingHashJoin<RowT> {
+ /**
+ * Creates node for FULL OUTER JOIN.
+ *
+ * @param ctx Execution context.
+ * @param rowType Row type.
+ * @param info Join info.
+ * @param outRowHnd Out row handler.
+ * @param leftRowFactory Left row factory.
+ * @param rightRowFactory Right row factory.
+ * @param nonEqCond If provided, only rows matching the predicate will
be emitted as matched rows.
+ */
+ private FullOuterRowTouchingHashJoin(
+ ExecutionContext<RowT> ctx,
+ RelDataType rowType,
+ IgniteJoinInfo info,
+ RowHandler<RowT> outRowHnd,
+ RowHandler.RowFactory<RowT> leftRowFactory,
+ RowHandler.RowFactory<RowT> rightRowFactory,
+ @Nullable BiPredicate<RowT, RowT> nonEqCond
+ ) {
+ super(ctx, rowType, info, outRowHnd, leftRowFactory,
rightRowFactory, nonEqCond);
+ }
+ }
+
+ /**
+ * Abstract base class for filtering rows of the left table based on
matching or non-matching conditions with the
+ * right table.
+ */
+ private abstract static class AbstractFilteringHashJoin<RowT> extends
AbstractStoringHashJoin<RowT, ArrayList<RowT>> {
+ /**
+ * Constructor.
+ *
+ * @param ctx Execution context.
+ * @param rowType Out row type.
+ * @param info Join info.
+ * @param nonEqCond Non-equi conditions.
+ */
+ private AbstractFilteringHashJoin(
+ ExecutionContext<RowT> ctx,
+ RelDataType rowType,
+ IgniteJoinInfo info,
+ @Nullable BiPredicate<RowT, RowT> nonEqCond
+ ) {
+ super(ctx, rowType, info, false, nonEqCond);
}
/** {@inheritDoc} */
@@ -640,19 +739,14 @@ public abstract class HashJoinNode<Row> extends
AbstractRightMaterializedJoinNod
try {
while (requested > 0 && (left != null ||
!leftInBuf.isEmpty())) {
- // Proceed with next left row, if previous was fully
processed.
- if (left == null) {
- left = leftInBuf.remove();
+ checkNextLeftRowStarted();
- rightIt = lookup(left).iterator();
- }
-
- boolean anyMatched = rightIt.hasNext() && nonEqCond ==
null;
+ boolean anyMatched = hasNextRight() && nonEqCond ==
null;
if (!anyMatched) {
- // Find any matched row.
- while (rightIt.hasNext()) {
- RowT right = rightIt.next();
+ // Find any matched right row.
+ while (hasNextRight()) {
+ RowT right = nextRight();
if (nonEqCond.test(left, right)) {
anyMatched = true;
@@ -666,15 +760,14 @@ public abstract class HashJoinNode<Row> extends
AbstractRightMaterializedJoinNod
}
if (anyMatched) {
- requested--;
-
- downstream().push(left);
+ onMatchesFound();
- rightIt = Collections.emptyIterator();
+ rightRows = null;
}
+ else
+ onMatchesNotFound();
- if (!rightIt.hasNext())
- left = null;
+ left = null;
}
}
finally {
@@ -687,67 +780,112 @@ public abstract class HashJoinNode<Row> extends
AbstractRightMaterializedJoinNod
tryToRequestInputs();
}
+
+ /** {@inheritDoc} */
+ @Override protected ArrayList<RowT> createRowList() {
+ return new ArrayList<>();
+ }
+
+ /** Triggered when matches found for current left row. */
+ protected void onMatchesFound() throws Exception {
+ // No-op.
+ }
+
+ /** Triggered when matches not found for current left row. */
+ protected void onMatchesNotFound() throws Exception {
+ // No-op.
+ }
+ }
+
+ /** */
+ private static final class SemiHashJoin<RowT> extends
AbstractFilteringHashJoin<RowT> {
+ /**
+ * Creates node for SEMI JOIN operator.
+ *
+ * @param ctx Execution context.
+ * @param rowType Out row type.
+ * @param info Join info.
+ * @param nonEqCond If provided, only rows matching the predicate will
be emitted as matched rows.
+ */
+ private SemiHashJoin(
+ ExecutionContext<RowT> ctx,
+ RelDataType rowType,
+ IgniteJoinInfo info,
+ @Nullable BiPredicate<RowT, RowT> nonEqCond
+ ) {
+ super(ctx, rowType, info, nonEqCond);
+ }
+
+ /** {@inheritDoc} */
+ @Override protected void onMatchesFound() throws Exception {
+ requested--;
+
+ downstream().push(left);
+ }
}
/** */
- private static final class AntiHashJoin<RowT> extends HashJoinNode<RowT> {
+ private static final class AntiHashJoin<RowT> extends
AbstractFilteringHashJoin<RowT> {
/**
* Creates node for ANTI JOIN.
*
* @param ctx Execution context.
* @param rowType Out row type.
* @param info Join info.
- * @param outRowHnd Output row handler.
* @param nonEqCond Non-equi conditions.
*/
private AntiHashJoin(
ExecutionContext<RowT> ctx,
RelDataType rowType,
IgniteJoinInfo info,
- RowHandler<RowT> outRowHnd,
@Nullable BiPredicate<RowT, RowT> nonEqCond
) {
- super(ctx, rowType, info, outRowHnd, false, nonEqCond);
+ super(ctx, rowType, info, nonEqCond);
}
/** {@inheritDoc} */
- @Override protected void join() throws Exception {
- if (waitingRight == NOT_WAITING) {
- inLoop = true;
+ @Override protected void onMatchesNotFound() throws Exception {
+ requested--;
- try {
- while (requested > 0 && (left != null ||
!leftInBuf.isEmpty())) {
- if (rescheduleJoin())
- return;
-
- left = leftInBuf.remove();
+ downstream().push(left);
+ }
+ }
- Collection<RowT> rightRows = lookup(left);
+ /** */
+ private static final class TouchableList<T> extends ArrayList<T> {
+ /** */
+ private boolean touched;
- if (rightRows.isEmpty()) {
- requested--;
+ /** */
+ public void touch() {
+ touched = true;
+ }
- downstream().push(left);
- }
+ /** */
+ public boolean touched() {
+ return touched;
+ }
+ }
- left = null;
- }
- }
- finally {
- inLoop = false;
- }
- }
+ /** */
+ private static final class RowTouchableList<T> extends ArrayList<T> {
+ /** */
+ private @Nullable BitSet touched;
- if (checkJoinFinished())
- return;
+ /** */
+ public void touch(int idx) {
+ if (touched == null)
+ touched = new BitSet(size());
- tryToRequestInputs();
+ touched.set(idx);
}
- }
- /** */
- private static final class TouchedArrayList<T> extends ArrayList<T> {
/** */
- private boolean touched;
+ public boolean touched(int idx) {
+ if (touched == null)
+ return false;
+
+ return touched.get(idx);
+ }
}
}
diff --git
a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rule/HashJoinConverterRule.java
b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rule/HashJoinConverterRule.java
index de1016d3c51..1a3cc988085 100644
---
a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rule/HashJoinConverterRule.java
+++
b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rule/HashJoinConverterRule.java
@@ -17,7 +17,6 @@
package org.apache.ignite.internal.processors.query.calcite.rule;
-import java.util.EnumSet;
import org.apache.calcite.plan.RelOptCluster;
import org.apache.calcite.plan.RelOptPlanner;
import org.apache.calcite.plan.RelOptRule;
@@ -25,7 +24,6 @@ import org.apache.calcite.plan.RelOptRuleCall;
import org.apache.calcite.plan.RelTraitSet;
import org.apache.calcite.rel.PhysicalNode;
import org.apache.calcite.rel.RelNode;
-import org.apache.calcite.rel.core.JoinRelType;
import org.apache.calcite.rel.logical.LogicalJoin;
import org.apache.calcite.rel.metadata.RelMetadataQuery;
import org.apache.ignite.internal.processors.query.calcite.hint.HintDefinition;
@@ -38,9 +36,6 @@ public class HashJoinConverterRule extends
AbstractIgniteJoinConverterRule {
/** */
public static final RelOptRule INSTANCE = new HashJoinConverterRule();
- /** */
- private static final EnumSet<JoinRelType> NON_EQ_CONDITIONS_SUPPORT =
EnumSet.of(JoinRelType.INNER, JoinRelType.SEMI);
-
/** Ctor. */
private HashJoinConverterRule() {
super("HashJoinConverter", HintDefinition.HASH_JOIN);
@@ -52,12 +47,7 @@ public class HashJoinConverterRule extends
AbstractIgniteJoinConverterRule {
IgniteJoinInfo joinInfo = IgniteJoinInfo.of(join);
- if (joinInfo.pairs().isEmpty())
- return false;
-
- // Current limitation: unmatched products on left or right part
requires special handling of non-equi condition
- // on execution level.
- return joinInfo.isEqui() ||
NON_EQ_CONDITIONS_SUPPORT.contains(join.getJoinType());
+ return !joinInfo.pairs().isEmpty();
}
/** {@inheritDoc} */
diff --git
a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/exec/rel/HashJoinExecutionTest.java
b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/exec/rel/HashJoinExecutionTest.java
index 25f1023d0b7..56d650f254c 100644
---
a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/exec/rel/HashJoinExecutionTest.java
+++
b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/exec/rel/HashJoinExecutionTest.java
@@ -19,7 +19,6 @@ package
org.apache.ignite.internal.processors.query.calcite.exec.rel;
import java.util.ArrayList;
import java.util.Arrays;
-import java.util.Comparator;
import java.util.function.BiPredicate;
import java.util.stream.IntStream;
import java.util.stream.Stream;
@@ -92,9 +91,9 @@ public class HashJoinExecutionTest extends
AbstractExecutionTest {
Object[][] expected = {
{1, 1, "Igor"},
- {1, 1, "Alexey"},
{2, 2, "Roman"},
{5, null, "Ivan"},
+ {1, 1, "Alexey"},
};
checkResults(expected, rows);
@@ -194,32 +193,331 @@ public class HashJoinExecutionTest extends
AbstractExecutionTest {
}
/** */
- @Test public void testInnerJoinWithPostFiltration() {
- doTestJoinWithPostFiltration(INNER, new Object[][] {{3, "Alexey", 1,
1, "Core"}});
+ @Test
+ public void testInnerJoinWithPostFiltration() {
+ Object[][] expected = {
+ // No rows for cases 0, 1, 2, 10 - empty left side, no rows for
cases, 0, 3, 6, 9 - empty right side.
+ {1, "Roman4", 4, 4, "SQL4"},
+ {1, "Roman5", 5, 5, "SQL5"},
+ {1, "Roman5", 5, 5, "QA5"},
+ {1, "Roman7", 7, 7, "SQL7"},
+ {2, "Ivan7", 7, 7, "SQL7"},
+ {1, "Roman8", 8, 8, "SQL8"},
+ {1, "Roman8", 8, 8, "QA8"},
+ {2, "Ivan8", 8, 8, "SQL8"},
+ {2, "Ivan8", 8, 8, "QA8"},
+ };
+
+ doTestJoinWithPostFiltration(INNER, expected);
+ }
+
+ /** */
+ @Test
+ public void testLeftJoinWithPostFiltration() {
+ Object[][] expected = {
+ // Case 0.
+ {0, "Igor0", 0, null, null},
+
+ // Case 1.
+ {0, "Igor1", 1, null, null},
+
+ // Case 2.
+ {0, "Igor2", 2, null, null},
+
+ // Case 3.
+ {0, "Igor3", 3, null, null},
+ {1, "Roman3", 3, null, null},
+
+ // Case 4.
+ {0, "Igor4", 4, null, null},
+ {1, "Roman4", 4, 4, "SQL4"},
+
+ // Case 5.
+ {0, "Igor5", 5, null, null},
+ {1, "Roman5", 5, 5, "SQL5"},
+ {1, "Roman5", 5, 5, "QA5"},
+
+ // Case 6.
+ {0, "Igor6", 6, null, null},
+ {1, "Roman6", 6, null, null},
+ {2, "Ivan6", 6, null, null},
+
+ // Case 7.
+ {0, "Igor7", 7, null, null},
+ {1, "Roman7", 7, 7, "SQL7"},
+ {2, "Ivan7", 7, 7, "SQL7"},
+
+ // Case 8.
+ {0, "Igor8", 8, null, null},
+ {1, "Roman8", 8, 8, "SQL8"},
+ {1, "Roman8", 8, 8, "QA8"},
+ {2, "Ivan8", 8, 8, "SQL8"},
+ {2, "Ivan8", 8, 8, "QA8"},
+
+ // Case 9.
+ {0, "Igor9", 9, null, null},
+ {1, "Roman9", 9, null, null},
+ {2, "Ivan9", 9, null, null},
+ };
+
+ doTestJoinWithPostFiltration(LEFT, expected);
+ }
+
+ /** */
+ @Test
+ public void testRightJoinWithPostFiltration() {
+ Object[][] expected = {
+ // Case 0.
+ {null, null, null, 0, "Core0"},
+
+ // Case 1.
+ {null, null, null, 1, "Core1"},
+ {null, null, null, 1, "SQL1"},
+
+ // Case 2.
+ {null, null, null, 2, "Core2"},
+ {null, null, null, 2, "SQL2"},
+ {null, null, null, 2, "QA2"},
+
+ // Case 3.
+ {null, null, null, 3, "Core3"},
+
+ // Case 4.
+ {1, "Roman4", 4, 4, "SQL4"},
+ {null, null, null, 4, "Core4"},
+
+ // Case 5.
+ {1, "Roman5", 5, 5, "SQL5"},
+ {1, "Roman5", 5, 5, "QA5"},
+ {null, null, null, 5, "Core5"},
+
+ // Case 6.
+ {null, null, null, 6, "Core6"},
+
+ // Case 7.
+ {1, "Roman7", 7, 7, "SQL7"},
+ {2, "Ivan7", 7, 7, "SQL7"},
+ {null, null, null, 7, "Core7"},
+
+ // Case 8.
+ {1, "Roman8", 8, 8, "SQL8"},
+ {1, "Roman8", 8, 8, "QA8"},
+ {2, "Ivan8", 8, 8, "SQL8"},
+ {2, "Ivan8", 8, 8, "QA8"},
+ {null, null, null, 8, "Core8"},
+
+ // Case 10.
+ {null, null, null, 10, "Core10"},
+ {null, null, null, 10, "SQL10"},
+ {null, null, null, 10, "QA10"},
+ };
+
+ doTestJoinWithPostFiltration(RIGHT, expected);
+ }
+
+ /** */
+ @Test
+ public void testFullJoinWithPostFiltration() {
+ Object[][] expected = {
+ // Case 0.
+ {0, "Igor0", 0, null, null},
+ {null, null, null, 0, "Core0"},
+
+ // Case 1.
+ {0, "Igor1", 1, null, null},
+ {null, null, null, 1, "Core1"},
+ {null, null, null, 1, "SQL1"},
+
+ // Case 2.
+ {0, "Igor2", 2, null, null},
+ {null, null, null, 2, "Core2"},
+ {null, null, null, 2, "SQL2"},
+ {null, null, null, 2, "QA2"},
+
+ // Case 3.
+ {0, "Igor3", 3, null, null},
+ {1, "Roman3", 3, null, null},
+ {null, null, null, 3, "Core3"},
+
+ // Case 4.
+ {0, "Igor4", 4, null, null},
+ {1, "Roman4", 4, 4, "SQL4"},
+ {null, null, null, 4, "Core4"},
+
+ // Case 5.
+ {0, "Igor5", 5, null, null},
+ {1, "Roman5", 5, 5, "SQL5"},
+ {1, "Roman5", 5, 5, "QA5"},
+ {null, null, null, 5, "Core5"},
+
+ // Case 6.
+ {0, "Igor6", 6, null, null},
+ {1, "Roman6", 6, null, null},
+ {2, "Ivan6", 6, null, null},
+ {null, null, null, 6, "Core6"},
+
+ // Case 7.
+ {0, "Igor7", 7, null, null},
+ {1, "Roman7", 7, 7, "SQL7"},
+ {2, "Ivan7", 7, 7, "SQL7"},
+ {null, null, null, 7, "Core7"},
+
+ // Case 8.
+ {0, "Igor8", 8, null, null},
+ {1, "Roman8", 8, 8, "SQL8"},
+ {1, "Roman8", 8, 8, "QA8"},
+ {2, "Ivan8", 8, 8, "SQL8"},
+ {2, "Ivan8", 8, 8, "QA8"},
+ {null, null, null, 8, "Core8"},
+
+ // Case 9.
+ {0, "Igor9", 9, null, null},
+ {1, "Roman9", 9, null, null},
+ {2, "Ivan9", 9, null, null},
+
+ // Case 10.
+ {null, null, null, 10, "Core10"},
+ {null, null, null, 10, "SQL10"},
+ {null, null, null, 10, "QA10"},
+ };
+
+ doTestJoinWithPostFiltration(FULL, expected);
}
/** */
@Test
public void testSemiJoinWithPostFiltration() {
- doTestJoinWithPostFiltration(SEMI, new Object[][] {{3, "Alexey", 1}});
+ Object[][] expected = {
+ // No rows for cases 0, 1, 2, 10 - empty left side, no rows for
cases, 0, 3, 6, 9 - empty right side.
+ {1, "Roman4", 4},
+ {1, "Roman5", 5},
+ {1, "Roman7", 7},
+ {2, "Ivan7", 7},
+ {1, "Roman8", 8},
+ {2, "Ivan8", 8},
+ };
+
+ doTestJoinWithPostFiltration(SEMI, expected);
+ }
+
+ /** */
+ @Test
+ public void testAntiJoinWithPostFiltration() {
+ Object[][] expected = {
+ {0, "Igor0", 0},
+ {0, "Igor1", 1},
+ {0, "Igor2", 2},
+ {0, "Igor3", 3},
+ {1, "Roman3", 3},
+ {0, "Igor4", 4},
+ {0, "Igor5", 5},
+ {0, "Igor6", 6},
+ {1, "Roman6", 6},
+ {2, "Ivan6", 6},
+ {0, "Igor7", 7},
+ {0, "Igor8", 8},
+ {0, "Igor9", 9},
+ {1, "Roman9", 9},
+ {2, "Ivan9", 9},
+ };
+
+ doTestJoinWithPostFiltration(ANTI, expected);
}
/** */
private void doTestJoinWithPostFiltration(JoinRelType joinType, Object[][]
expected) {
Object[][] persons = {
- new Object[] {0, "Igor", 1},
- new Object[] {1, "Roman", 2},
- new Object[] {2, "Ivan", 5},
- new Object[] {3, "Alexey", 1}
+ // Case 0: 1 rows on left (-1 filtered) to 1 rows on right (-1
filtered).
+ {0, "Igor0", 0},
+
+ // Case 1: 1 rows on left (-1 filtered) to 2 rows on right (-1
filtered).
+ {0, "Igor1", 1},
+
+ // Case 2: 1 rows on left (-1 filtered) to 3 rows on right (-1
filtered).
+ {0, "Igor2", 2},
+
+ // Case 3: 2 rows on left (-1 filtered) to 1 rows on right (-1
filtered).
+ {0, "Igor3", 3},
+ {1, "Roman3", 3},
+
+ // Case 4: 2 rows on left (-1 filtered) to 2 rows on right (-1
filtered).
+ {0, "Igor4", 4},
+ {1, "Roman4", 4},
+
+ // Case 5: 2 rows on left (-1 filtered) to 3 rows on right (-1
filtered).
+ {0, "Igor5", 5},
+ {1, "Roman5", 5},
+
+ // Case 6: 3 rows on left (-1 filtered) to 1 rows on right (-1
filtered).
+ {0, "Igor6", 6},
+ {1, "Roman6", 6},
+ {2, "Ivan6", 6},
+
+ // Case 7: 3 rows on left (-1 filtered) to 2 rows on right (-1
filtered).
+ {0, "Igor7", 7},
+ {1, "Roman7", 7},
+ {2, "Ivan7", 7},
+
+ // Case 8: 3 rows on left (-1 filtered) to 3 rows on right (-1
filtered).
+ {0, "Igor8", 8},
+ {1, "Roman8", 8},
+ {2, "Ivan8", 8},
+
+ // Case 9: 3 rows on left (-1 filtered) to 0 rows on right.
+ {0, "Igor9", 9},
+ {1, "Roman9", 9},
+ {2, "Ivan9", 9},
+
+ // Case 10: 0 rows on left to 3 rows on right (-1 filtered).
};
Object[][] deps = {
- new Object[] {1, "Core"},
- new Object[] {2, "SQL"},
- new Object[] {3, "QA"}
+ // Case 0: 1 rows on left (-1 filtered) to 1 rows on right (-1
filtered).
+ {0, "Core0"},
+
+ // Case 1: 1 rows on left (-1 filtered) to 2 rows on right (-1
filtered).
+ {1, "Core1"},
+ {1, "SQL1"},
+
+ // Case 2: 1 rows on left (-1 filtered) to 3 rows on right (-1
filtered).
+ {2, "Core2"},
+ {2, "SQL2"},
+ {2, "QA2"},
+
+ // Case 3: 2 rows on left (-1 filtered) to 1 rows on right (-1
filtered).
+ {3, "Core3"},
+
+ // Case 4: 2 rows on left (-1 filtered) to 2 rows on right (-1
filtered).
+ {4, "Core4"},
+ {4, "SQL4"},
+
+ // Case 5: 2 rows on left (-1 filtered) to 3 rows on right (-1
filtered).
+ {5, "Core5"},
+ {5, "SQL5"},
+ {5, "QA5"},
+
+ // Case 6: 3 rows on left (-1 filtered) to 1 rows on right (-1
filtered).
+ {6, "Core6"},
+
+ // Case 7: 3 rows on left (-1 filtered) to 2 rows on right (-1
filtered).
+ {7, "Core7"},
+ {7, "SQL7"},
+
+ // Case 8: 3 rows on left (-1 filtered) to 3 rows on right (-1
filtered).
+ {8, "Core8"},
+ {8, "SQL8"},
+ {8, "QA8"},
+
+ // Case 9: 3 rows on left (-1 filtered) to 0 rows on right.
+
+ // Case 10: 0 rows on left to 3 rows on right (-1 filtered).
+ {10, "Core10"},
+ {10, "SQL10"},
+ {10, "QA10"},
};
- BiPredicate<Object[], Object[]> condition = (l, r) ->
((CharSequence)r[1]).length() > 3 && ((CharSequence)l[1]).length() > 4;
+ BiPredicate<Object[], Object[]> condition =
+ (l, r) -> !((String)l[1]).startsWith("Igor") &&
!((String)r[1]).startsWith("Core");
validate(joinType, Stream.of(persons)::iterator,
Stream.of(deps)::iterator, expected, -1, condition);
}
@@ -395,7 +693,8 @@ public class HashJoinExecutionTest extends
AbstractExecutionTest {
private static void checkResults(Object[][] expected, ArrayList<Object[]>
actual) {
assertEquals(expected.length, actual.size());
- actual.sort(Comparator.comparing(r -> (int)r[0]));
+ actual.sort(F::compareArrays);
+ Arrays.sort(expected, F::compareArrays);
int length = expected.length;
@@ -403,8 +702,7 @@ public class HashJoinExecutionTest extends
AbstractExecutionTest {
Object[] exp = expected[i];
Object[] act = actual.get(i);
- assertEquals(exp.length, act.length);
- assertEquals(0, F.compareArrays(exp, act));
+ assertEqualsArraysAware(exp, act);
}
}
}
diff --git
a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/integration/JoinIntegrationTest.java
b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/integration/JoinIntegrationTest.java
index 1343afe37b3..f115ae3ea15 100644
---
a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/integration/JoinIntegrationTest.java
+++
b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/integration/JoinIntegrationTest.java
@@ -342,8 +342,7 @@ public class JoinIntegrationTest extends
AbstractBasicIntegrationTransactionalTe
.returns(null, 2, 2, null)
.check();
- // HASH JOIN doesn't support: completely non-equi conditions,
additional non-equi conditions
- // except INNER and SEMI joins.
+ // HASH JOIN doesn't support: completely non-equi conditions.
// MERGE JOIN doesn't support: non-equi conditions.
if (joinType == JoinType.HASH || joinType == JoinType.MERGE)
return;
@@ -622,10 +621,8 @@ public class JoinIntegrationTest extends
AbstractBasicIntegrationTransactionalTe
.returns(null, 2, 2, null)
.check();
- // HASH JOIN doesn't support: completely non-equi conditions,
additional non-equi conditions
- // except INNER and SEMI joins.
// MERGE JOIN doesn't support: non-equi conditions.
- if (joinType == JoinType.MERGE || joinType == JoinType.HASH)
+ if (joinType == JoinType.MERGE)
return;
assertQuery("select t1.c2, t1.c3, t2.c3 from t1 left join t2 on t1.c2
is not distinct from t2.c3 and t1.c3 > 3" +
@@ -648,6 +645,10 @@ public class JoinIntegrationTest extends
AbstractBasicIntegrationTransactionalTe
.returns(null, 2, 5, null)
.check();
+ // HASH JOIN doesn't support: completely non-equi conditions.
+ if (joinType == JoinType.HASH)
+ return;
+
assertQuery("select t1.c1, t2.c1 from t1 left join t2 on t1.c1=4 order
by t1.c1, t2.c1")
.returns(1, null)
.returns(2, null)
@@ -968,10 +969,8 @@ public class JoinIntegrationTest extends
AbstractBasicIntegrationTransactionalTe
.returns(null, 2, 2, null)
.check();
- // HASH JOIN doesn't support: completely non-equi conditions,
additional non-equi conditions
- // except INNER and SEMI joins.
// MERGE JOIN doesn't support: non-equi conditions.
- if (joinType == JoinType.MERGE || joinType == JoinType.HASH)
+ if (joinType == JoinType.MERGE)
return;
assertQuery("select t1.c2, t1.c3, t2.c3 from t1 right join t2 on t1.c2
is not distinct from t2.c3 and t1.c3 > 3" +
@@ -994,6 +993,10 @@ public class JoinIntegrationTest extends
AbstractBasicIntegrationTransactionalTe
.returns(null, null, null, 3)
.check();
+ // HASH JOIN doesn't support: completely non-equi conditions.
+ if (joinType == JoinType.HASH)
+ return;
+
assertQuery("select t1.c1, t2.c1 from t1 right join t2 on t1.c1=4
order by t1.c1, t2.c1")
.returns(4, 1)
.returns(4, 2)
@@ -1086,10 +1089,8 @@ public class JoinIntegrationTest extends
AbstractBasicIntegrationTransactionalTe
.returns(null, 2, 2, null)
.check();
- // HASH JOIN doesn't support: completely non-equi conditions,
additional non-equi conditions
- // except INNER and SEMI joins.
// MERGE JOIN doesn't support: non-equi conditions.
- if (joinType == JoinType.MERGE || joinType == JoinType.HASH)
+ if (joinType == JoinType.MERGE)
return;
assertQuery("select t1.c2, t1.c3, t2.c3 from t1 full join t2 on t1.c2
is not distinct from t2.c3 and t1.c3 > 3" +
@@ -1118,6 +1119,10 @@ public class JoinIntegrationTest extends
AbstractBasicIntegrationTransactionalTe
.returns(null, null, null, 3)
.check();
+ // HASH JOIN doesn't support: completely non-equi conditions.
+ if (joinType == JoinType.HASH)
+ return;
+
assertQuery("select t1.c1, t2.c1 from t1 full join t2 on t1.c1=4 order
by t1.c1, t2.c1")
.returns(1, null)
.returns(2, null)
diff --git
a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/planner/HashJoinPlannerTest.java
b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/planner/HashJoinPlannerTest.java
index 88a95c20fce..97a8689535e 100644
---
a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/planner/HashJoinPlannerTest.java
+++
b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/planner/HashJoinPlannerTest.java
@@ -112,28 +112,27 @@ public class HashJoinPlannerTest extends
AbstractPlannerTest {
public void testHashJoinApplied() throws Exception {
// Parms: request, can be planned, only INNER or SEMI join.
List<List<Object>> testParams = F.asList(
- F.asList("select t1.c1 from t1 %s join t2 on t1.id = t2.id", true,
false),
- F.asList("select t1.c1 from t1 %s join t2 on t1.id = t2.id and
t1.c1=t2.c1", true, false),
- F.asList("select t1.c1 from t1 %s join t2 using(c1)", true, false),
- F.asList("select t1.c1 from t1 %s join t2 on t1.c1 = 1", false,
false),
- F.asList("select t1.c1 from t1 %s join t2 ON t1.id is not distinct
from t2.c1", true, false),
- F.asList("select t1.c1 from t1 %s join t2 ON t1.id is not distinct
from t2.c1 and t1.c1 is not distinct from t2.id",
- true, false),
- F.asList("select t1.c1 from t1 %s join t2 ON t1.id is not distinct
from t2.c1 and t1.c1 = t2.id", true, false),
- F.asList("select t1.c1 from t1 %s join t2 ON t1.id is not distinct
from t2.c1 and t1.c1 > t2.id", true, true),
- F.asList("select t1.c1 from t1 %s join t2 on t1.c1 = ?", false,
false),
- F.asList("select t1.c1 from t1 %s join t2 on t1.c1 =
OCTET_LENGTH('TEST')", false, false),
- F.asList("select t1.c1 from t1 %s join t2 on t1.c1 =
LOG10(t1.c1)", false, false),
- F.asList("select t1.c1 from t1 %s join t2 on t1.c1 = t2.c1 and
t1.ID > t2.ID", true, true),
- F.asList("select t1.c1 from t1 %s join t2 on t1.c1 = 1 and t2.c1 =
1", false, false)
+ F.asList("select t1.c1 from t1 %s join t2 on t1.id = t2.id", true),
+ F.asList("select t1.c1 from t1 %s join t2 on t1.id = t2.id and
t1.c1=t2.c1", true),
+ F.asList("select t1.c1 from t1 %s join t2 using(c1)", true),
+ F.asList("select t1.c1 from t1 %s join t2 on t1.c1 = 1", false),
+ F.asList("select t1.c1 from t1 %s join t2 ON t1.id is not distinct
from t2.c1", true),
+ F.asList("select t1.c1 from t1 %s join t2 ON t1.id is not distinct
from t2.c1 and t1.c1 is not distinct from t2.id",
+ true),
+ F.asList("select t1.c1 from t1 %s join t2 ON t1.id is not distinct
from t2.c1 and t1.c1 = t2.id", true),
+ F.asList("select t1.c1 from t1 %s join t2 ON t1.id is not distinct
from t2.c1 and t1.c1 > t2.id", true),
+ F.asList("select t1.c1 from t1 %s join t2 on t1.c1 = ?", false),
+ F.asList("select t1.c1 from t1 %s join t2 on t1.c1 =
OCTET_LENGTH('TEST')", false),
+ F.asList("select t1.c1 from t1 %s join t2 on t1.c1 =
LOG10(t1.c1)", false),
+ F.asList("select t1.c1 from t1 %s join t2 on t1.c1 = t2.c1 and
t1.ID > t2.ID", true),
+ F.asList("select t1.c1 from t1 %s join t2 on t1.c1 = 1 and t2.c1 =
1", false)
);
for (List<Object> paramSet : testParams) {
- assert paramSet != null && paramSet.size() == 3;
+ assert paramSet != null && paramSet.size() == 2;
String sql = (String)paramSet.get(0);
boolean canBePlanned = (Boolean)paramSet.get(1);
- boolean onlyInnerOrSemi = (Boolean)paramSet.get(2);
TestTable tbl1 = createTable("T1", IgniteDistributions.single(),
"ID", Integer.class, "C1", Integer.class);
TestTable tbl2 = createTable("T2", IgniteDistributions.single(),
"ID", Integer.class, "C1", Integer.class);
@@ -141,9 +140,6 @@ public class HashJoinPlannerTest extends
AbstractPlannerTest {
IgniteSchema schema = createSchema(tbl1, tbl2);
for (String joinType : JOIN_TYPES) {
- if (onlyInnerOrSemi && !joinType.equals("INNER") &&
!joinType.equals("SEMI"))
- continue;
-
String sql0 = String.format(sql, joinType);
if (log.isInfoEnabled())