You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@calcite.apache.org by jh...@apache.org on 2014/12/30 16:41:22 UTC

incubator-calcite git commit: [CALCITE-451] Implement theta join, inner and outer, in enumerable convention

Repository: incubator-calcite
Updated Branches:
  refs/heads/master 0caa1c546 -> 167623f40


[CALCITE-451] Implement theta join, inner and outer, in enumerable convention


Project: http://git-wip-us.apache.org/repos/asf/incubator-calcite/repo
Commit: http://git-wip-us.apache.org/repos/asf/incubator-calcite/commit/167623f4
Tree: http://git-wip-us.apache.org/repos/asf/incubator-calcite/tree/167623f4
Diff: http://git-wip-us.apache.org/repos/asf/incubator-calcite/diff/167623f4

Branch: refs/heads/master
Commit: 167623f40797f051450c2d8dda5b26998317bd94
Parents: 0caa1c5
Author: julianhyde <jh...@apache.org>
Authored: Tue Dec 30 00:24:32 2014 -0800
Committer: julianhyde <jh...@apache.org>
Committed: Tue Dec 30 00:24:32 2014 -0800

----------------------------------------------------------------------
 .../adapter/enumerable/EnumerableJoinRule.java  |  14 +-
 .../adapter/enumerable/EnumerableThetaJoin.java | 178 +++++++++++++++++++
 .../org/apache/calcite/runtime/Enumerables.java |  59 +++++-
 .../org/apache/calcite/util/BuiltInMethod.java  |   2 +
 .../apache/calcite/runtime/EnumerablesTest.java |  97 ++++++++--
 .../java/org/apache/calcite/test/JdbcTest.java  |  21 +++
 core/src/test/resources/sql/outer.oq            |  41 +++++
 .../calcite/linq4j/tree/FunctionExpression.java |  27 ++-
 8 files changed, 419 insertions(+), 20 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-calcite/blob/167623f4/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableJoinRule.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableJoinRule.java b/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableJoinRule.java
index 915b635..6ffc912 100644
--- a/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableJoinRule.java
+++ b/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableJoinRule.java
@@ -18,6 +18,7 @@ package org.apache.calcite.adapter.enumerable;
 
 import org.apache.calcite.plan.Convention;
 import org.apache.calcite.plan.RelOptCluster;
+import org.apache.calcite.plan.RelTraitSet;
 import org.apache.calcite.rel.InvalidRelException;
 import org.apache.calcite.rel.RelNode;
 import org.apache.calcite.rel.convert.ConverterRule;
@@ -53,15 +54,24 @@ class EnumerableJoinRule extends ConverterRule {
       }
       newInputs.add(input);
     }
+    final RelOptCluster cluster = join.getCluster();
+    final RelTraitSet traitSet =
+        join.getTraitSet().replace(EnumerableConvention.INSTANCE);
     final RelNode left = newInputs.get(0);
     final RelNode right = newInputs.get(1);
     final JoinInfo info = JoinInfo.of(left, right, join.getCondition());
     if (!info.isEqui() && join.getJoinType() != JoinRelType.INNER) {
       // EnumerableJoinRel only supports equi-join. We can put a filter on top
       // if it is an inner join.
-      return null;
+      try {
+        return new EnumerableThetaJoin(cluster, traitSet, left, right,
+            join.getCondition(), join.getJoinType(),
+            join.getVariablesStopped());
+      } catch (InvalidRelException e) {
+        EnumerableRules.LOGGER.fine(e.toString());
+        return null;
+      }
     }
-    final RelOptCluster cluster = join.getCluster();
     RelNode newRel;
     try {
       newRel = new EnumerableJoin(

http://git-wip-us.apache.org/repos/asf/incubator-calcite/blob/167623f4/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableThetaJoin.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableThetaJoin.java b/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableThetaJoin.java
new file mode 100644
index 0000000..bf4516a
--- /dev/null
+++ b/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableThetaJoin.java
@@ -0,0 +1,178 @@
+/*
+ * 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.calcite.adapter.enumerable;
+
+import org.apache.calcite.linq4j.function.Predicate2;
+import org.apache.calcite.linq4j.tree.BlockBuilder;
+import org.apache.calcite.linq4j.tree.Expression;
+import org.apache.calcite.linq4j.tree.Expressions;
+import org.apache.calcite.linq4j.tree.ParameterExpression;
+import org.apache.calcite.plan.RelOptCluster;
+import org.apache.calcite.plan.RelOptCost;
+import org.apache.calcite.plan.RelOptPlanner;
+import org.apache.calcite.plan.RelTraitSet;
+import org.apache.calcite.rel.InvalidRelException;
+import org.apache.calcite.rel.RelNode;
+import org.apache.calcite.rel.core.Join;
+import org.apache.calcite.rel.core.JoinRelType;
+import org.apache.calcite.rel.metadata.RelMetadataQuery;
+import org.apache.calcite.rex.RexNode;
+import org.apache.calcite.rex.RexProgramBuilder;
+import org.apache.calcite.util.BuiltInMethod;
+import org.apache.calcite.util.Pair;
+
+import com.google.common.collect.ImmutableList;
+
+import java.util.Set;
+
+/** Implementation of {@link org.apache.calcite.rel.core.Join} in
+ * {@link org.apache.calcite.adapter.enumerable.EnumerableConvention enumerable calling convention}
+ * that allows conditions that are not just {@code =} (equals). */
+public class EnumerableThetaJoin extends Join implements EnumerableRel {
+  protected EnumerableThetaJoin(RelOptCluster cluster, RelTraitSet traits,
+      RelNode left, RelNode right, RexNode condition, JoinRelType joinType,
+      Set<String> variablesStopped) throws InvalidRelException {
+    super(cluster, traits, left, right, condition, joinType,
+        variablesStopped);
+  }
+
+  @Override public EnumerableThetaJoin copy(RelTraitSet traitSet,
+      RexNode condition, RelNode left, RelNode right, JoinRelType joinType,
+      boolean semiJoinDone) {
+    try {
+      return new EnumerableThetaJoin(getCluster(), traitSet, left, right,
+          condition, joinType, variablesStopped);
+    } catch (InvalidRelException e) {
+      // Semantic error not possible. Must be a bug. Convert to
+      // internal error.
+      throw new AssertionError(e);
+    }
+  }
+
+  @Override public RelOptCost computeSelfCost(RelOptPlanner planner) {
+    double rowCount = RelMetadataQuery.getRowCount(this);
+
+    // Joins can be flipped, and for many algorithms, both versions are viable
+    // and have the same cost. To make the results stable between versions of
+    // the planner, make one of the versions slightly more expensive.
+    switch (joinType) {
+    case RIGHT:
+      rowCount = addEpsilon(rowCount);
+      break;
+    default:
+      if (left.getId() > right.getId()) {
+        rowCount = addEpsilon(rowCount);
+      }
+    }
+
+    final double rightRowCount = right.getRows();
+    final double leftRowCount = left.getRows();
+    if (Double.isInfinite(leftRowCount)) {
+      rowCount = leftRowCount;
+    }
+    if (Double.isInfinite(rightRowCount)) {
+      rowCount = rightRowCount;
+    }
+    return planner.getCostFactory().makeCost(rowCount, 0, 0);
+  }
+
+  private double addEpsilon(double d) {
+    assert d >= 0d;
+    final double d0 = d;
+    if (d < 10) {
+      // For small d, adding 1 would change the value significantly.
+      d *= 1.001d;
+      if (d != d0) {
+        return d;
+      }
+    }
+    // For medium d, add 1. Keeps integral values integral.
+    ++d;
+    if (d != d0) {
+      return d;
+    }
+    // For large d, adding 1 might not change the value. Add .1%.
+    // If d is NaN, this still will probably not change the value. That's OK.
+    d *= 1.001d;
+    return d;
+  }
+
+  public Result implement(EnumerableRelImplementor implementor, Prefer pref) {
+    final BlockBuilder builder = new BlockBuilder();
+    final Result leftResult =
+        implementor.visitChild(this, 0, (EnumerableRel) left, pref);
+    Expression leftExpression =
+        builder.append("left", leftResult.block);
+    final Result rightResult =
+        implementor.visitChild(this, 1, (EnumerableRel) right, pref);
+    Expression rightExpression =
+        builder.append("right", rightResult.block);
+    final PhysType physType =
+        PhysTypeImpl.of(implementor.getTypeFactory(),
+            getRowType(),
+            pref.preferArray());
+    final BlockBuilder builder2 = new BlockBuilder();
+    return implementor.result(
+        physType,
+        builder.append(
+            Expressions.call(BuiltInMethod.THETA_JOIN.method,
+                leftExpression,
+                rightExpression,
+                predicate(implementor,
+                    builder2,
+                    leftResult.physType,
+                    rightResult.physType,
+                    condition),
+                EnumUtils.joinSelector(joinType,
+                    physType,
+                    ImmutableList.of(leftResult.physType,
+                        rightResult.physType)),
+                Expressions.constant(joinType.generatesNullsOnLeft()),
+                Expressions.constant(joinType.generatesNullsOnRight())))
+            .toBlock());
+  }
+
+  Expression predicate(EnumerableRelImplementor implementor,
+      BlockBuilder builder, PhysType leftPhysType, PhysType rightPhysType,
+      RexNode condition) {
+    final ParameterExpression left_ =
+        Expressions.parameter(leftPhysType.getJavaRowType(), "left");
+    final ParameterExpression right_ =
+        Expressions.parameter(rightPhysType.getJavaRowType(), "right");
+    final RexProgramBuilder program =
+        new RexProgramBuilder(
+            implementor.getTypeFactory().builder()
+                .addAll(left.getRowType().getFieldList())
+                .addAll(right.getRowType().getFieldList())
+                .build(),
+            getCluster().getRexBuilder());
+    program.addCondition(condition);
+    builder.add(
+        Expressions.return_(null,
+            RexToLixTranslator.translateCondition(program.getProgram(),
+                implementor.getTypeFactory(),
+                builder,
+                new RexToLixTranslator.InputGetterImpl(
+                    ImmutableList.of(Pair.of((Expression) left_, leftPhysType),
+                        Pair.of((Expression) right_, rightPhysType))),
+                implementor.allCorrelateVariables)));
+    return Expressions.lambda(Predicate2.class, builder.toBlock(), left_,
+        right_);
+  }
+}
+
+// End EnumerableThetaJoin.java

http://git-wip-us.apache.org/repos/asf/incubator-calcite/blob/167623f4/core/src/main/java/org/apache/calcite/runtime/Enumerables.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/calcite/runtime/Enumerables.java b/core/src/main/java/org/apache/calcite/runtime/Enumerables.java
index 629bced..c8ebe4d 100644
--- a/core/src/main/java/org/apache/calcite/runtime/Enumerables.java
+++ b/core/src/main/java/org/apache/calcite/runtime/Enumerables.java
@@ -25,12 +25,15 @@ import org.apache.calcite.linq4j.function.EqualityComparer;
 import org.apache.calcite.linq4j.function.Function1;
 import org.apache.calcite.linq4j.function.Function2;
 import org.apache.calcite.linq4j.function.Predicate1;
+import org.apache.calcite.linq4j.function.Predicate2;
 import org.apache.calcite.util.Bug;
 
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Lists;
+import com.google.common.collect.Sets;
 
 import java.util.List;
+import java.util.Set;
 
 /**
  * Utilities for processing {@link org.apache.calcite.linq4j.Enumerable}
@@ -104,6 +107,55 @@ public class Enumerables {
   }
 
   /**
+   * Correlates the elements of two sequences based on a predicate.
+   */
+  public static <TSource, TInner, TResult> Enumerable<TResult> thetaJoin(
+      final Enumerable<TSource> outer, final Enumerable<TInner> inner,
+      final Predicate2<TSource, TInner> predicate,
+      Function2<TSource, TInner, TResult> resultSelector,
+      final boolean generateNullsOnLeft,
+      final boolean generateNullsOnRight) {
+    // Building the result as a list is easy but hogs memory. We should iterate.
+    final List<TResult> result = Lists.newArrayList();
+    final Enumerator<TSource> lefts = outer.enumerator();
+    final List<TInner> rightList = inner.toList();
+    final Set<TInner> rightUnmatched;
+    if (generateNullsOnLeft) {
+      rightUnmatched = Sets.newIdentityHashSet();
+      rightUnmatched.addAll(rightList);
+    } else {
+      rightUnmatched = null;
+    }
+    while (lefts.moveNext()) {
+      int leftMatchCount = 0;
+      final TSource left = lefts.current();
+      final Enumerator<TInner> rights = Linq4j.iterableEnumerator(rightList);
+      while (rights.moveNext()) {
+        TInner right = rights.current();
+        if (predicate.apply(left, right)) {
+          ++leftMatchCount;
+          if (rightUnmatched != null) {
+            rightUnmatched.remove(right);
+          }
+          result.add(resultSelector.apply(left, right));
+        }
+      }
+      if (generateNullsOnRight && leftMatchCount == 0) {
+        result.add(resultSelector.apply(left, null));
+      }
+    }
+    if (rightUnmatched != null) {
+      final Enumerator<TInner> rights =
+          Linq4j.iterableEnumerator(rightUnmatched);
+      while (rights.moveNext()) {
+        TInner right = rights.current();
+        result.add(resultSelector.apply(null, right));
+      }
+    }
+    return Linq4j.asEnumerable(result);
+  }
+
+  /**
    * Filters a sequence of values based on a
    * predicate.
    */
@@ -254,8 +306,11 @@ public class Enumerables {
 
           public TResult current() {
             final List<Object> list = cartesians.current();
-            return resultSelector.apply((TSource) list.get(0),
-                (TInner) list.get(1));
+            @SuppressWarnings("unchecked") final TSource left =
+                (TSource) list.get(0);
+            @SuppressWarnings("unchecked") final TInner right =
+                (TInner) list.get(1);
+            return resultSelector.apply(left, right);
           }
 
           public boolean moveNext() {

http://git-wip-us.apache.org/repos/asf/incubator-calcite/blob/167623f4/core/src/main/java/org/apache/calcite/util/BuiltInMethod.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/calcite/util/BuiltInMethod.java b/core/src/main/java/org/apache/calcite/util/BuiltInMethod.java
index ad83a92..5ef8a89 100644
--- a/core/src/main/java/org/apache/calcite/util/BuiltInMethod.java
+++ b/core/src/main/java/org/apache/calcite/util/BuiltInMethod.java
@@ -124,6 +124,8 @@ public enum BuiltInMethod {
   SLICE0(Enumerables.class, "slice0", Enumerable.class),
   SEMI_JOIN(Enumerables.class, "semiJoin", Enumerable.class, Enumerable.class,
       Function1.class, Function1.class),
+  THETA_JOIN(Enumerables.class, "thetaJoin", Enumerable.class, Enumerable.class,
+      Predicate2.class, Function2.class, boolean.class, boolean.class),
   CORRELATE_JOIN(ExtendedEnumerable.class, "correlateJoin",
       CorrelateJoinType.class, Function1.class, Function2.class),
   SELECT(ExtendedEnumerable.class, "select", Function1.class),

http://git-wip-us.apache.org/repos/asf/incubator-calcite/blob/167623f4/core/src/test/java/org/apache/calcite/runtime/EnumerablesTest.java
----------------------------------------------------------------------
diff --git a/core/src/test/java/org/apache/calcite/runtime/EnumerablesTest.java b/core/src/test/java/org/apache/calcite/runtime/EnumerablesTest.java
index 6a5fd1b..bfbf339 100644
--- a/core/src/test/java/org/apache/calcite/runtime/EnumerablesTest.java
+++ b/core/src/test/java/org/apache/calcite/runtime/EnumerablesTest.java
@@ -21,6 +21,7 @@ import org.apache.calcite.linq4j.Linq4j;
 import org.apache.calcite.linq4j.function.Function1;
 import org.apache.calcite.linq4j.function.Function2;
 import org.apache.calcite.linq4j.function.Functions;
+import org.apache.calcite.linq4j.function.Predicate2;
 
 import com.google.common.collect.Lists;
 
@@ -36,19 +37,39 @@ import static org.junit.Assert.assertThat;
  * Unit tests for {@link org.apache.calcite.runtime.Enumerables}.
  */
 public class EnumerablesTest {
+  private static final Enumerable<Emp> EMPS = Linq4j.asEnumerable(
+      Arrays.asList(
+          new Emp(10, "Fred"),
+          new Emp(20, "Theodore"),
+          new Emp(20, "Sebastian"),
+          new Emp(30, "Joe")));
+
+  private static final Enumerable<Dept> DEPTS = Linq4j.asEnumerable(
+      Arrays.asList(
+          new Dept(20, "Sales"),
+          new Dept(15, "Marketing")));
+
+  private static final Function2<Emp, Dept, String> EMP_DEPT_TO_STRING =
+      new Function2<Emp, Dept, String>() {
+        public String apply(Emp v0, Dept v1) {
+          return "{" + (v0 == null ? null : v0.name)
+              + ", " + (v0 == null ? null : v0.deptno)
+              + ", " + (v1 == null ? null : v1.deptno)
+              + ", " + (v1 == null ? null : v1.name)
+              + "}";
+        }
+      };
+
+  private static final Predicate2<Emp, Dept> EQUAL_DEPTNO =
+      new Predicate2<Emp, Dept>() {
+        public boolean apply(Emp v0, Dept v1) {
+          return v0.deptno == v1.deptno;
+        }
+      };
+
   @Test public void testSemiJoin() {
     assertThat(
-        Enumerables.semiJoin(
-            Linq4j.asEnumerable(
-                Arrays.asList(
-                    new Emp(10, "Fred"),
-                    new Emp(20, "Theodore"),
-                    new Emp(20, "Sebastian"),
-                    new Emp(30, "Joe"))),
-            Linq4j.asEnumerable(
-                Arrays.asList(
-                    new Dept(20, "Sales"),
-                    new Dept(15, "Marketing"))),
+        Enumerables.semiJoin(EMPS, DEPTS,
             new Function1<Emp, Integer>() {
               public Integer apply(Emp a0) {
                 return a0.deptno;
@@ -162,6 +183,60 @@ public class EnumerablesTest {
         }, false, false);
   }
 
+  @Test public void testThetaJoin() {
+    assertThat(
+        Enumerables.thetaJoin(EMPS, DEPTS, EQUAL_DEPTNO, EMP_DEPT_TO_STRING,
+            false, false).toList().toString(),
+        equalTo("[{Theodore, 20, 20, Sales}, {Sebastian, 20, 20, Sales}]"));
+  }
+
+  @Test public void testThetaLeftJoin() {
+    assertThat(
+        Enumerables.thetaJoin(EMPS, DEPTS, EQUAL_DEPTNO, EMP_DEPT_TO_STRING,
+            false, true).toList().toString(),
+        equalTo("[{Fred, 10, null, null}, {Theodore, 20, 20, Sales}, "
+            + "{Sebastian, 20, 20, Sales}, {Joe, 30, null, null}]"));
+  }
+
+  @Test public void testThetaRightJoin() {
+    assertThat(
+        Enumerables.thetaJoin(EMPS, DEPTS, EQUAL_DEPTNO, EMP_DEPT_TO_STRING,
+            true, false).toList().toString(),
+        equalTo("[{Theodore, 20, 20, Sales}, {Sebastian, 20, 20, Sales}, "
+            + "{null, null, 15, Marketing}]"));
+  }
+
+  @Test public void testThetaFullJoin() {
+    assertThat(
+        Enumerables.thetaJoin(EMPS, DEPTS, EQUAL_DEPTNO, EMP_DEPT_TO_STRING,
+            true, true).toList().toString(),
+        equalTo("[{Fred, 10, null, null}, {Theodore, 20, 20, Sales}, "
+            + "{Sebastian, 20, 20, Sales}, {Joe, 30, null, null}, "
+            + "{null, null, 15, Marketing}]"));
+  }
+
+  @Test public void testThetaFullJoinLeftEmpty() {
+    assertThat(
+        Enumerables.thetaJoin(EMPS.take(0), DEPTS, EQUAL_DEPTNO,
+            EMP_DEPT_TO_STRING, true, true).toList().toString(),
+        equalTo("[{null, null, 20, Sales}, {null, null, 15, Marketing}]"));
+  }
+
+  @Test public void testThetaFullJoinRightEmpty() {
+    assertThat(
+        Enumerables.thetaJoin(EMPS, DEPTS.take(0), EQUAL_DEPTNO,
+            EMP_DEPT_TO_STRING, true, true).toList().toString(),
+        equalTo("[{Fred, 10, null, null}, {Theodore, 20, null, null}, "
+            + "{Sebastian, 20, null, null}, {Joe, 30, null, null}]"));
+  }
+
+  @Test public void testThetaFullJoinBothEmpty() {
+    assertThat(
+        Enumerables.thetaJoin(EMPS.take(0), DEPTS.take(0), EQUAL_DEPTNO,
+            EMP_DEPT_TO_STRING, true, true).toList().toString(),
+        equalTo("[]"));
+  }
+
   /** Employee record. */
   private static class Emp {
     final int deptno;

http://git-wip-us.apache.org/repos/asf/incubator-calcite/blob/167623f4/core/src/test/java/org/apache/calcite/test/JdbcTest.java
----------------------------------------------------------------------
diff --git a/core/src/test/java/org/apache/calcite/test/JdbcTest.java b/core/src/test/java/org/apache/calcite/test/JdbcTest.java
index e166631..6a44e01 100644
--- a/core/src/test/java/org/apache/calcite/test/JdbcTest.java
+++ b/core/src/test/java/org/apache/calcite/test/JdbcTest.java
@@ -1587,6 +1587,27 @@ public class JdbcTest {
   }
 
   /** Test case for
+   * <a href="https://issues.apache.org/jira/browse/CALCITE-451">[CALCITE-451]
+   * Implement theta join, inner and outer, in enumerable convention</a>. */
+  @Test public void testThetaJoin() {
+    CalciteAssert.that()
+        .with(CalciteAssert.Config.REGULAR)
+        .query(
+            "select e.\"empid\", d.\"name\", e.\"name\"\n"
+            + "from \"hr\".\"emps\" as e\n"
+            + "left join \"hr\".\"depts\" as d\n"
+            + "on e.\"deptno\" < d.\"deptno\"\n")
+        .returnsUnordered("empid=100; name=Marketing; name=Bill",
+            "empid=100; name=HR; name=Bill",
+            "empid=200; name=Marketing; name=Eric",
+            "empid=200; name=HR; name=Eric",
+            "empid=150; name=Marketing; name=Sebastian",
+            "empid=150; name=HR; name=Sebastian",
+            "empid=110; name=Marketing; name=Theodore",
+            "empid=110; name=HR; name=Theodore");
+  }
+
+  /** Test case for
    * <a href="https://issues.apache.org/jira/browse/CALCITE-35">CALCITE-35</a>,
    * "Support parenthesized sub-clause in JOIN". */
   @Ignore

http://git-wip-us.apache.org/repos/asf/incubator-calcite/blob/167623f4/core/src/test/resources/sql/outer.oq
----------------------------------------------------------------------
diff --git a/core/src/test/resources/sql/outer.oq b/core/src/test/resources/sql/outer.oq
index e371fe9..92071b1 100644
--- a/core/src/test/resources/sql/outer.oq
+++ b/core/src/test/resources/sql/outer.oq
@@ -228,6 +228,47 @@ select * from (select * from emp where gender ='F') as emp full join dept on emp
 
 !ok
 
+# same as above, but expressed as a theta-join
+select * from (select * from emp where gender ='F') as emp full join dept on emp.deptno - dept.deptno = 0;
++-------+--------+--------+---------+-------------+
+| ENAME | DEPTNO | GENDER | DEPTNO0 | DNAME       |
++-------+--------+--------+---------+-------------+
+| Jane  |     10 | F      |      10 | Sales       |
+| Susan |     30 | F      |      30 | Engineering |
+| Alice |     30 | F      |      30 | Engineering |
+| Eve   |     50 | F      |         |             |
+| Grace |     60 | F      |         |             |
+| Wilma |        | F      |         |             |
+|       |        |        |      20 | Marketing   |
+|       |        |        |      40 | Empty       |
++-------+--------+--------+---------+-------------+
+(8 rows)
+
+!ok
+EnumerableThetaJoin(condition=[=(-($1, $3), 0)], joinType=[full])
+  EnumerableCalc(expr#0..2=[{inputs}], expr#3=['F'], expr#4=[=($t2, $t3)], proj#0..2=[{exprs}], $condition=[$t4])
+    EnumerableUnion(all=[true])
+      EnumerableCalc(expr#0=[{inputs}], expr#1=['Jane'], expr#2=[10], expr#3=['F'], EXPR$0=[$t1], EXPR$1=[$t2], EXPR$2=[$t3])
+        EnumerableValues(tuples=[[{ 0 }]])
+      EnumerableCalc(expr#0=[{inputs}], expr#1=['Bob'], expr#2=[10], expr#3=['M'], EXPR$0=[$t1], EXPR$1=[$t2], EXPR$2=[$t3])
+        EnumerableValues(tuples=[[{ 0 }]])
+      EnumerableCalc(expr#0=[{inputs}], expr#1=['Eric'], expr#2=[20], expr#3=['M'], EXPR$0=[$t1], EXPR$1=[$t2], EXPR$2=[$t3])
+        EnumerableValues(tuples=[[{ 0 }]])
+      EnumerableCalc(expr#0=[{inputs}], expr#1=['Susan'], expr#2=[30], expr#3=['F'], EXPR$0=[$t1], EXPR$1=[$t2], EXPR$2=[$t3])
+        EnumerableValues(tuples=[[{ 0 }]])
+      EnumerableCalc(expr#0=[{inputs}], expr#1=['Alice'], expr#2=[30], expr#3=['F'], EXPR$0=[$t1], EXPR$1=[$t2], EXPR$2=[$t3])
+        EnumerableValues(tuples=[[{ 0 }]])
+      EnumerableCalc(expr#0=[{inputs}], expr#1=['Adam'], expr#2=[50], expr#3=['M'], EXPR$0=[$t1], EXPR$1=[$t2], EXPR$2=[$t3])
+        EnumerableValues(tuples=[[{ 0 }]])
+      EnumerableCalc(expr#0=[{inputs}], expr#1=['Eve'], expr#2=[50], expr#3=['F'], EXPR$0=[$t1], EXPR$1=[$t2], EXPR$2=[$t3])
+        EnumerableValues(tuples=[[{ 0 }]])
+      EnumerableCalc(expr#0=[{inputs}], expr#1=['Grace'], expr#2=[60], expr#3=['F'], EXPR$0=[$t1], EXPR$1=[$t2], EXPR$2=[$t3])
+        EnumerableValues(tuples=[[{ 0 }]])
+      EnumerableCalc(expr#0=[{inputs}], expr#1=['Wilma'], expr#2=[null], expr#3=['F'], EXPR$0=[$t1], EXPR$1=[$t2], EXPR$2=[$t3])
+        EnumerableValues(tuples=[[{ 0 }]])
+  EnumerableCalc(expr#0..1=[{inputs}], proj#0..1=[{exprs}])
+    EnumerableValues(tuples=[[{ 10, 'Sales      ' }, { 20, 'Marketing  ' }, { 30, 'Engineering' }, { 40, 'Empty      ' }]])
+!plan
 
 
 # End outer.oq

http://git-wip-us.apache.org/repos/asf/incubator-calcite/blob/167623f4/linq4j/src/main/java/org/apache/calcite/linq4j/tree/FunctionExpression.java
----------------------------------------------------------------------
diff --git a/linq4j/src/main/java/org/apache/calcite/linq4j/tree/FunctionExpression.java b/linq4j/src/main/java/org/apache/calcite/linq4j/tree/FunctionExpression.java
index 705693c..cc681df 100644
--- a/linq4j/src/main/java/org/apache/calcite/linq4j/tree/FunctionExpression.java
+++ b/linq4j/src/main/java/org/apache/calcite/linq4j/tree/FunctionExpression.java
@@ -172,10 +172,14 @@ public final class FunctionExpression<F extends Function<?>>
 
     // Generate an intermediate bridge method if at least one parameter is
     // primitive.
+    final String bridgeResultTypeName =
+        isAbstractMethodPrimitive()
+            ? Types.className(bridgeResultType)
+            : Types.boxClassName(bridgeResultType);
     if (!boxBridgeParams.equals(params)) {
       writer
           .append("public ")
-          .append(Types.boxClassName(bridgeResultType))
+          .append(bridgeResultTypeName)
           .list(" " + methodName + "(", ", ", ") ", boxBridgeParams)
           .begin("{\n")
           .list("return " + methodName + "(\n", ",\n", ");\n", boxBridgeArgs)
@@ -190,7 +194,7 @@ public final class FunctionExpression<F extends Function<?>>
     if (!bridgeParams.equals(params)) {
       writer
         .append("public ")
-        .append(Types.boxClassName(bridgeResultType))
+        .append(bridgeResultTypeName)
         .list(" " + methodName + "(", ", ", ") ", bridgeParams)
         .begin("{\n")
         .list("return " + methodName + "(\n", ",\n", ");\n", bridgeArgs)
@@ -200,11 +204,24 @@ public final class FunctionExpression<F extends Function<?>>
     writer.end("}\n");
   }
 
+  private boolean isAbstractMethodPrimitive() {
+    Method method = getAbstractMethod();
+    return method != null && Primitive.is(method.getReturnType());
+  }
+
   private String getAbstractMethodName() {
-    if (type.toString().contains("CalciteFlatMapFunction")) {
-      return "call"; // FIXME
+    final Method abstractMethod = getAbstractMethod();
+    assert abstractMethod != null;
+    return abstractMethod.getName();
+  }
+
+  private Method getAbstractMethod() {
+    if (type instanceof Class
+      && ((Class) type).isInterface()
+      && ((Class) type).getDeclaredMethods().length == 1) {
+      return ((Class) type).getDeclaredMethods()[0];
     }
-    return "apply";
+    return null;
   }
 
   @Override public boolean equals(Object o) {