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 2023/08/21 01:03:42 UTC

[calcite] 02/02: [CALCITE-5941] Support LITERAL_AGG in Interpreter

This is an automated email from the ASF dual-hosted git repository.

jhyde pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/calcite.git

commit 0d3456705a13e26e73f51a7ea978e817a1d00154
Author: Julian Hyde <jh...@apache.org>
AuthorDate: Sun Aug 20 00:36:39 2023 -0700

    [CALCITE-5941] Support LITERAL_AGG in Interpreter
    
    Also clean up ScannableTableTest, declaring the hitherto hidden third
    column of the "beatles" table.
---
 .../apache/calcite/interpreter/AggregateNode.java  | 174 ++++++++++++---------
 .../org/apache/calcite/test/InterpreterTest.java   |  27 +++-
 .../apache/calcite/test/ScannableTableTest.java    | 128 ++++++++-------
 3 files changed, 186 insertions(+), 143 deletions(-)

diff --git a/core/src/main/java/org/apache/calcite/interpreter/AggregateNode.java b/core/src/main/java/org/apache/calcite/interpreter/AggregateNode.java
index 9f9db72dcc..e461b238de 100644
--- a/core/src/main/java/org/apache/calcite/interpreter/AggregateNode.java
+++ b/core/src/main/java/org/apache/calcite/interpreter/AggregateNode.java
@@ -38,6 +38,8 @@ import org.apache.calcite.rex.RexNode;
 import org.apache.calcite.runtime.FunctionContexts;
 import org.apache.calcite.schema.FunctionContext;
 import org.apache.calcite.schema.impl.AggregateFunctionImpl;
+import org.apache.calcite.sql.SqlAggFunction;
+import org.apache.calcite.sql.fun.SqlInternalOperators;
 import org.apache.calcite.sql.fun.SqlStdOperatorTable;
 import org.apache.calcite.sql.validate.SqlConformance;
 import org.apache.calcite.sql.validate.SqlConformanceEnum;
@@ -91,7 +93,8 @@ public class AggregateNode extends AbstractSingleNode<Aggregate> {
     ImmutableList.Builder<AccumulatorFactory> builder = ImmutableList.builder();
     for (AggregateCall aggregateCall : rel.getAggCallList()) {
       @SuppressWarnings("method.invocation.invalid")
-      AccumulatorFactory accumulator = getAccumulator(aggregateCall, false);
+      AccumulatorFactory accumulator =
+          getAccumulator(compiler, aggregateCall, false);
       builder.add(accumulator);
     }
     accumulatorFactories = builder.build();
@@ -110,91 +113,32 @@ public class AggregateNode extends AbstractSingleNode<Aggregate> {
     }
   }
 
-  private AccumulatorFactory getAccumulator(final AggregateCall call,
-      boolean ignoreFilter) {
+  private AccumulatorFactory getAccumulator(Compiler compiler,
+      final AggregateCall call, boolean ignoreFilter) {
     if (call.filterArg >= 0 && !ignoreFilter) {
-      final AccumulatorFactory factory = getAccumulator(call, true);
+      final AccumulatorFactory factory = getAccumulator(compiler, call, true);
       return () -> {
         final Accumulator accumulator = factory.get();
         return new FilterAccumulator(accumulator, call.filterArg);
       };
     }
-    if (call.getAggregation() == SqlStdOperatorTable.COUNT) {
+    final SqlAggFunction op = call.getAggregation();
+    if (op == SqlStdOperatorTable.COUNT) {
       return () -> new CountAccumulator(call);
-    } else if (call.getAggregation() == SqlStdOperatorTable.SUM
-        || call.getAggregation() == SqlStdOperatorTable.SUM0) {
-      final Class<?> clazz;
-      switch (call.type.getSqlTypeName()) {
-      case DOUBLE:
-      case REAL:
-      case FLOAT:
-        clazz = DoubleSum.class;
-        break;
-      case DECIMAL:
-        clazz = BigDecimalSum.class;
-        break;
-      case INTEGER:
-        clazz = IntSum.class;
-        break;
-      case BIGINT:
-      default:
-        clazz = LongSum.class;
-        break;
-      }
-      if (call.getAggregation() == SqlStdOperatorTable.SUM) {
-        return new UdaAccumulatorFactory(getAggFunction(clazz), call, true,
-            dataContext);
-      } else {
-        return new UdaAccumulatorFactory(getAggFunction(clazz), call, false,
-            dataContext);
-      }
-    } else if (call.getAggregation() == SqlStdOperatorTable.MIN) {
-      final Class<?> clazz;
-      switch (call.getType().getSqlTypeName()) {
-      case INTEGER:
-        clazz = MinInt.class;
-        break;
-      case FLOAT:
-        clazz = MinFloat.class;
-        break;
-      case DOUBLE:
-      case REAL:
-        clazz = MinDouble.class;
-        break;
-      case DECIMAL:
-        clazz = MinBigDecimal.class;
-        break;
-      case BOOLEAN:
-        clazz = MinBoolean.class;
-        break;
-      default:
-        clazz = MinLong.class;
-        break;
-      }
-      return new UdaAccumulatorFactory(getAggFunction(clazz), call, true,
-          dataContext);
-    } else if (call.getAggregation() == SqlStdOperatorTable.MAX) {
-      final Class<?> clazz;
-      switch (call.getType().getSqlTypeName()) {
-      case INTEGER:
-        clazz = MaxInt.class;
-        break;
-      case FLOAT:
-        clazz = MaxFloat.class;
-        break;
-      case DOUBLE:
-      case REAL:
-        clazz = MaxDouble.class;
-        break;
-      case DECIMAL:
-        clazz = MaxBigDecimal.class;
-        break;
-      default:
-        clazz = MaxLong.class;
-        break;
-      }
+    } else if (op == SqlStdOperatorTable.SUM
+        || op == SqlStdOperatorTable.SUM0) {
+      final Class<?> clazz = sumClass(call);
+      return new UdaAccumulatorFactory(getAggFunction(clazz), call,
+          op == SqlStdOperatorTable.SUM, dataContext);
+    } else if (op == SqlStdOperatorTable.MAX
+        || op == SqlStdOperatorTable.MIN) {
+      final Class<?> clazz = maxMinClass(call);
       return new UdaAccumulatorFactory(getAggFunction(clazz), call, true,
           dataContext);
+    } else if (op == SqlInternalOperators.LITERAL_AGG) {
+      final Scalar scalar = compiler.compile(call.rexList, null);
+      final Object value = scalar.execute(compiler.createContext());
+      return () -> new LiteralAccumulator(value);
     } else {
       final JavaTypeFactory typeFactory =
           (JavaTypeFactory) rel.getCluster().getTypeFactory();
@@ -269,6 +213,41 @@ public class AggregateNode extends AbstractSingleNode<Aggregate> {
     }
   }
 
+  private static Class<?> maxMinClass(AggregateCall call) {
+    boolean max = call.getAggregation() == SqlStdOperatorTable.MAX;
+    switch (call.getType().getSqlTypeName()) {
+    case INTEGER:
+      return max ? MaxInt.class : MinInt.class;
+    case FLOAT:
+      return max ? MaxFloat.class : MinFloat.class;
+    case DOUBLE:
+    case REAL:
+      return max ? MaxDouble.class : MinDouble.class;
+    case DECIMAL:
+      return max ? MaxBigDecimal.class : MinBigDecimal.class;
+    case BOOLEAN:
+      return max ? MaxBoolean.class : MinBoolean.class;
+    default:
+      return max ? MaxLong.class : MinLong.class;
+    }
+  }
+
+  private static Class<?> sumClass(AggregateCall call) {
+    switch (call.type.getSqlTypeName()) {
+    case DOUBLE:
+    case REAL:
+    case FLOAT:
+      return DoubleSum.class;
+    case DECIMAL:
+      return BigDecimalSum.class;
+    case INTEGER:
+      return IntSum.class;
+    case BIGINT:
+    default:
+      return LongSum.class;
+    }
+  }
+
   private static AggregateFunctionImpl getAggFunction(Class<?> clazz) {
     return requireNonNull(
         AggregateFunctionImpl.create(clazz),
@@ -303,6 +282,22 @@ public class AggregateNode extends AbstractSingleNode<Aggregate> {
     }
   }
 
+  /** Accumulator for calls to the LITERAL_AGG function. */
+  private static class LiteralAccumulator implements Accumulator {
+    private final @Nullable Object value;
+
+    LiteralAccumulator(@Nullable Object value) {
+      this.value = value;
+    }
+
+    @Override public void send(Row row) {
+    }
+
+    @Override public @Nullable Object end() {
+      return value;
+    }
+  }
+
   /** Creates an {@link Accumulator}. */
   private interface AccumulatorFactory extends Supplier<Accumulator> {
   }
@@ -616,7 +611,7 @@ public class AggregateNode extends AbstractSingleNode<Aggregate> {
     }
 
     public Boolean add(Boolean accumulator, Boolean value) {
-      return accumulator.compareTo(value) < 0 ? accumulator : value;
+      return accumulator && value;
     }
 
     public Boolean merge(Boolean accumulator0, Boolean accumulator1) {
@@ -677,6 +672,29 @@ public class AggregateNode extends AbstractSingleNode<Aggregate> {
     }
   }
 
+  /** Implementation of {@code MAX} function to calculate the maximum of
+   * {@code boolean} values as a user-defined aggregate.
+   */
+  public static class MaxBoolean {
+    public MaxBoolean() { }
+
+    public Boolean init() {
+      return Boolean.FALSE;
+    }
+
+    public Boolean add(Boolean accumulator, Boolean value) {
+      return accumulator || value;
+    }
+
+    public Boolean merge(Boolean accumulator0, Boolean accumulator1) {
+      return add(accumulator0, accumulator1);
+    }
+
+    public Boolean result(Boolean accumulator) {
+      return accumulator;
+    }
+  }
+
   /** Accumulator factory based on a user-defined aggregate function. */
   private static class UdaAccumulatorFactory implements AccumulatorFactory {
     final AggregateFunctionImpl aggFunction;
diff --git a/core/src/test/java/org/apache/calcite/test/InterpreterTest.java b/core/src/test/java/org/apache/calcite/test/InterpreterTest.java
index f7e428e4bb..a98a19625b 100644
--- a/core/src/test/java/org/apache/calcite/test/InterpreterTest.java
+++ b/core/src/test/java/org/apache/calcite/test/InterpreterTest.java
@@ -292,7 +292,8 @@ class InterpreterTest {
   @Test void testInterpretScannableTable() {
     rootSchema.add("beatles", new ScannableTableTest.BeatlesTable());
     sql("select * from \"beatles\" order by \"i\"")
-        .returnsRows("[4, John]", "[4, Paul]", "[5, Ringo]", "[6, George]");
+        .returnsRows("[4, John, 1940]", "[4, Paul, 1942]", "[5, Ringo, 1940]",
+            "[6, George, 1943]");
   }
 
   /** Tests executing a plan on a
@@ -349,6 +350,22 @@ class InterpreterTest {
             "[Ringo, 1]");
   }
 
+  /** Tests a GROUP BY query that uses
+   * {@link org.apache.calcite.sql.fun.SqlInternalOperators#LITERAL_AGG}. */
+  @Test void testAggregateLiteralAgg() {
+    rootSchema.add("beatles", new ScannableTableTest.BeatlesTable());
+    final Function<RelBuilder, RelNode> relFn =
+        b -> b.scan("beatles")
+            .aggregate(b.groupKey("k"),
+                b.count().as("c"),
+                b.literalAgg(true).as("t"),
+                b.literalAgg(-3).as("minus3"))
+            .build();
+    fixture().withRel(relFn)
+        .returnsRows("[1940, 2, true, -3]", "[1942, 1, true, -3]",
+            "[1943, 1, true, -3]");
+  }
+
   /** Tests executing a plan on a single-column
    * {@link org.apache.calcite.schema.ScannableTable} using an interpreter. */
   @Test void testInterpretSimpleScannableTable() {
@@ -730,11 +747,11 @@ class InterpreterTest {
   /** Tests projecting zero fields. */
   @Test void testZeroFields() {
     final List<RexLiteral> row = ImmutableList.of();
-    fixture()
-        .withRel(b ->
-            b.values(ImmutableList.of(row, row),
+    final Function<RelBuilder, RelNode> relFn =
+        b -> b.values(ImmutableList.of(row, row),
                 b.getTypeFactory().builder().build())
-                .build())
+            .build();
+    fixture().withRel(relFn)
         .returnsRows("[]", "[]");
   }
 }
diff --git a/core/src/test/java/org/apache/calcite/test/ScannableTableTest.java b/core/src/test/java/org/apache/calcite/test/ScannableTableTest.java
index 747e69555a..ee0e54919d 100644
--- a/core/src/test/java/org/apache/calcite/test/ScannableTableTest.java
+++ b/core/src/test/java/org/apache/calcite/test/ScannableTableTest.java
@@ -28,6 +28,7 @@ import org.apache.calcite.rex.RexInputRef;
 import org.apache.calcite.rex.RexLiteral;
 import org.apache.calcite.rex.RexNode;
 import org.apache.calcite.runtime.Hook;
+import org.apache.calcite.runtime.PairList;
 import org.apache.calcite.schema.FilterableTable;
 import org.apache.calcite.schema.ProjectableFilterableTable;
 import org.apache.calcite.schema.ScannableTable;
@@ -67,52 +68,55 @@ import static org.hamcrest.Matchers.hasToString;
 import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
+import static java.util.Objects.requireNonNull;
+
 /**
  * Unit test for {@link org.apache.calcite.schema.ScannableTable}.
  */
 public class ScannableTableTest {
-  @Test void testTens() throws SQLException {
-    final Enumerator<Object[]> cursor = tens();
-    assertTrue(cursor.moveNext());
-    assertThat(cursor.current()[0], equalTo((Object) 0));
-    assertThat(cursor.current().length, equalTo(1));
-    assertTrue(cursor.moveNext());
-    assertThat(cursor.current()[0], equalTo((Object) 10));
-    assertTrue(cursor.moveNext());
-    assertThat(cursor.current()[0], equalTo((Object) 20));
-    assertTrue(cursor.moveNext());
-    assertThat(cursor.current()[0], equalTo((Object) 30));
-    assertFalse(cursor.moveNext());
+  @Test void testTens() {
+    try (Enumerator<Object[]> cursor = tens()) {
+      assertTrue(cursor.moveNext());
+      assertThat(cursor.current()[0], equalTo(0));
+      assertThat(cursor.current().length, equalTo(1));
+      assertTrue(cursor.moveNext());
+      assertThat(cursor.current()[0], equalTo(10));
+      assertTrue(cursor.moveNext());
+      assertThat(cursor.current()[0], equalTo(20));
+      assertTrue(cursor.moveNext());
+      assertThat(cursor.current()[0], equalTo(30));
+      assertFalse(cursor.moveNext());
+    }
   }
 
   /** A table with one column. */
-  @Test void testSimple() throws Exception {
+  @Test void testSimple() {
     CalciteAssert.that()
-        .with(newSchema("s", Pair.of("simple", new SimpleTable())))
+        .with(newSchema("s", PairList.of("simple", new SimpleTable())))
         .query("select * from \"s\".\"simple\"")
         .returnsUnordered("i=0", "i=10", "i=20", "i=30");
   }
 
   /** A table with two columns. */
-  @Test void testSimple2() throws Exception {
+  @Test void testSimple2() {
     CalciteAssert.that()
-        .with(newSchema("s", Pair.of("beatles", new BeatlesTable())))
+        .with(newSchema("s", PairList.of("beatles", new BeatlesTable())))
         .query("select * from \"s\".\"beatles\"")
-        .returnsUnordered("i=4; j=John",
-            "i=4; j=Paul",
-            "i=6; j=George",
-            "i=5; j=Ringo");
+        .returnsUnordered("i=4; j=John; k=1940",
+            "i=4; j=Paul; k=1942",
+            "i=6; j=George; k=1943",
+            "i=5; j=Ringo; k=1940");
   }
 
   /** A filter on a {@link FilterableTable} with two columns (cooperative). */
-  @Test void testFilterableTableCooperative() throws Exception {
+  @Test void testFilterableTableCooperative() {
     final StringBuilder buf = new StringBuilder();
     final Table table = new BeatlesFilterableTable(buf, true);
     final String explain = "PLAN="
         + "EnumerableInterpreter\n"
         + "  BindableTableScan(table=[[s, beatles]], filters=[[=($0, 4)]])";
     CalciteAssert.that()
-        .with(newSchema("s", Pair.of("beatles", table)))
+        .with(newSchema("s", PairList.of("beatles", table)))
         .query("select * from \"s\".\"beatles\" where \"i\" = 4")
         .explainContains(explain)
         .returnsUnordered("i=4; j=John; k=1940",
@@ -123,14 +127,14 @@ public class ScannableTableTest {
   }
 
   /** A filter on a {@link FilterableTable} with two columns (noncooperative). */
-  @Test void testFilterableTableNonCooperative() throws Exception {
+  @Test void testFilterableTableNonCooperative() {
     final StringBuilder buf = new StringBuilder();
     final Table table = new BeatlesFilterableTable(buf, false);
     final String explain = "PLAN="
         + "EnumerableInterpreter\n"
         + "  BindableTableScan(table=[[s, beatles2]], filters=[[=($0, 4)]])";
     CalciteAssert.that()
-        .with(newSchema("s", Pair.of("beatles2", table)))
+        .with(newSchema("s", PairList.of("beatles2", table)))
         .query("select * from \"s\".\"beatles2\" where \"i\" = 4")
         .explainContains(explain)
         .returnsUnordered("i=4; j=John; k=1940",
@@ -140,14 +144,14 @@ public class ScannableTableTest {
 
   /** A filter on a {@link org.apache.calcite.schema.ProjectableFilterableTable}
    * with two columns (cooperative). */
-  @Test void testProjectableFilterableCooperative() throws Exception {
+  @Test void testProjectableFilterableCooperative() {
     final StringBuilder buf = new StringBuilder();
     final Table table = new BeatlesProjectableFilterableTable(buf, true);
     final String explain = "PLAN="
         + "EnumerableInterpreter\n"
         + "  BindableTableScan(table=[[s, beatles]], filters=[[=($0, 4)]], projects=[[1]])";
     CalciteAssert.that()
-        .with(newSchema("s", Pair.of("beatles", table)))
+        .with(newSchema("s", PairList.of("beatles", table)))
         .query("select \"j\" from \"s\".\"beatles\" where \"i\" = 4")
         .explainContains(explain)
         .returnsUnordered("j=John",
@@ -158,14 +162,14 @@ public class ScannableTableTest {
         hasToString("returnCount=2, filter=<0, 4>, projects=[1, 0]"));
   }
 
-  @Test void testProjectableFilterableNonCooperative() throws Exception {
+  @Test void testProjectableFilterableNonCooperative() {
     final StringBuilder buf = new StringBuilder();
     final Table table = new BeatlesProjectableFilterableTable(buf, false);
     final String explain = "PLAN="
         + "EnumerableInterpreter\n"
         + "  BindableTableScan(table=[[s, beatles2]], filters=[[=($0, 4)]], projects=[[1]]";
     CalciteAssert.that()
-        .with(newSchema("s", Pair.of("beatles2", table)))
+        .with(newSchema("s", PairList.of("beatles2", table)))
         .query("select \"j\" from \"s\".\"beatles2\" where \"i\" = 4")
         .explainContains(explain)
         .returnsUnordered("j=John",
@@ -175,14 +179,14 @@ public class ScannableTableTest {
 
   /** A filter on a {@link org.apache.calcite.schema.ProjectableFilterableTable}
    * with two columns, and a project in the query. (Cooperative)*/
-  @Test void testProjectableFilterableWithProjectAndFilter() throws Exception {
+  @Test void testProjectableFilterableWithProjectAndFilter() {
     final StringBuilder buf = new StringBuilder();
     final Table table = new BeatlesProjectableFilterableTable(buf, true);
     final String explain = "PLAN="
         + "EnumerableInterpreter\n"
         + "  BindableTableScan(table=[[s, beatles]], filters=[[=($0, 4)]], projects=[[2, 1]]";
     CalciteAssert.that()
-        .with(newSchema("s", Pair.of("beatles", table)))
+        .with(newSchema("s", PairList.of("beatles", table)))
         .query("select \"k\",\"j\" from \"s\".\"beatles\" where \"i\" = 4")
         .explainContains(explain)
         .returnsUnordered("k=1940; j=John",
@@ -193,8 +197,7 @@ public class ScannableTableTest {
 
   /** A filter on a {@link org.apache.calcite.schema.ProjectableFilterableTable}
    * with two columns, and a project in the query (NonCooperative). */
-  @Test void testProjectableFilterableWithProjectFilterNonCooperative()
-      throws Exception {
+  @Test void testProjectableFilterableWithProjectFilterNonCooperative() {
     final StringBuilder buf = new StringBuilder();
     final Table table = new BeatlesProjectableFilterableTable(buf, false);
     final String explain = "PLAN="
@@ -202,7 +205,7 @@ public class ScannableTableTest {
         + "  BindableTableScan(table=[[s, beatles]], filters=[[>($2, 1941)]], "
         + "projects=[[0, 2]])";
     CalciteAssert.that()
-        .with(newSchema("s", Pair.of("beatles", table)))
+        .with(newSchema("s", PairList.of("beatles", table)))
         .query("select \"i\",\"k\" from \"s\".\"beatles\" where \"k\" > 1941")
         .explainContains(explain)
         .returnsUnordered("i=4; k=1942",
@@ -215,13 +218,13 @@ public class ScannableTableTest {
    * {@link org.apache.calcite.schema.ProjectableFilterableTable}. The table
    * refuses to execute the filter, so Calcite should add a pull up and
    * transform the filter (projecting the column needed by the filter). */
-  @Test void testPFTableRefusesFilterCooperative() throws Exception {
+  @Test void testPFTableRefusesFilterCooperative() {
     final StringBuilder buf = new StringBuilder();
     final Table table = new BeatlesProjectableFilterableTable(buf, false);
     final String explain = "PLAN=EnumerableInterpreter\n"
         + "  BindableTableScan(table=[[s, beatles2]], filters=[[=($0, 4)]], projects=[[2]])";
     CalciteAssert.that()
-        .with(newSchema("s", Pair.of("beatles2", table)))
+        .with(newSchema("s", PairList.of("beatles2", table)))
         .query("select \"k\" from \"s\".\"beatles2\" where \"i\" = 4")
         .explainContains(explain)
         .returnsUnordered("k=1940",
@@ -237,7 +240,7 @@ public class ScannableTableTest {
         + "  EnumerableInterpreter\n"
         + "    BindableTableScan(table=[[s, beatles]], filters=[[>($0, 1)]], projects=[[2]])";
     CalciteAssert.that()
-        .with(newSchema("s", Pair.of("beatles", table)))
+        .with(newSchema("s", PairList.of("beatles", table)))
         .query("select max(\"k\") as m from \"s\".\"beatles\" where \"i\" > 1")
         .explainContains(explain)
         .returnsUnordered("M=1943");
@@ -256,7 +259,7 @@ public class ScannableTableTest {
         + "    BindableTableScan(table=[[s, beatles]], filters=[[>($2, 1900)]], "
         + "projects=[[0]])";
     CalciteAssert.that()
-        .with(newSchema("s", Pair.of("beatles", table)))
+        .with(newSchema("s", PairList.of("beatles", table)))
         .query(sql)
         .explainContains(explain)
         .returnsUnordered("i=4; C=2",
@@ -278,13 +281,14 @@ public class ScannableTableTest {
         + "    EnumerableInterpreter\n"
         + "      BindableTableScan(table=[[s, beatles]], filters=[[=($2, 1940)]], projects=[[2, 0]])";
     CalciteAssert.that()
-        .with(newSchema("s", Pair.of("beatles", table)))
+        .with(newSchema("s", PairList.of("beatles", table)))
         .query(sql)
         .explainContains(explain)
         .returnsUnordered("k=1940; C=2");
   }
 
-  private static Pair<Integer, Object> getFilter(boolean cooperative, List<RexNode> filters) {
+  private static @Nullable Pair<Integer, Object> getFilter(boolean cooperative,
+      List<RexNode> filters) {
     final Iterator<RexNode> filterIter = filters.iterator();
     while (filterIter.hasNext()) {
       final RexNode node = filterIter.next();
@@ -312,14 +316,14 @@ public class ScannableTableTest {
    * <a href="https://issues.apache.org/jira/browse/CALCITE-458">[CALCITE-458]
    * ArrayIndexOutOfBoundsException when using just a single column in
    * interpreter</a>. */
-  @Test void testPFTableRefusesFilterSingleColumn() throws Exception {
+  @Test void testPFTableRefusesFilterSingleColumn() {
     final StringBuilder buf = new StringBuilder();
     final Table table = new BeatlesProjectableFilterableTable(buf, false);
     final String explain = "PLAN="
         + "EnumerableInterpreter\n"
         + "  BindableTableScan(table=[[s, beatles2]], filters=[[>($2, 1941)]], projects=[[2]])";
     CalciteAssert.that()
-        .with(newSchema("s", Pair.of("beatles2", table)))
+        .with(newSchema("s", PairList.of("beatles2", table)))
         .query("select \"k\" from \"s\".\"beatles2\" where \"k\" > 1941")
         .explainContains(explain)
         .returnsUnordered("k=1942",
@@ -330,7 +334,7 @@ public class ScannableTableTest {
   /** Test case for
    * <a href="https://issues.apache.org/jira/browse/CALCITE-3405">[CALCITE-3405]
    * Prune columns for ProjectableFilterable when project is not simple mapping</a>. */
-  @Test void testPushNonSimpleMappingProject() throws Exception {
+  @Test void testPushNonSimpleMappingProject() {
     final StringBuilder buf = new StringBuilder();
     final Table table = new BeatlesProjectableFilterableTable(buf, true);
     final String explain = "PLAN="
@@ -339,7 +343,7 @@ public class ScannableTableTest {
         + "  EnumerableInterpreter\n"
         + "    BindableTableScan(table=[[s, beatles]], projects=[[2, 0]])";
     CalciteAssert.that()
-        .with(newSchema("s", Pair.of("beatles", table)))
+        .with(newSchema("s", PairList.of("beatles", table)))
         .query("select \"k\", \"i\", \"k\", \"i\"+\"i\" \"ii\", 3 from \"s\".\"beatles\"")
         .explainContains(explain)
         .returnsUnordered(
@@ -353,7 +357,7 @@ public class ScannableTableTest {
   /** Test case for
    * <a href="https://issues.apache.org/jira/browse/CALCITE-3405">[CALCITE-3405]
    * Prune columns for ProjectableFilterable when project is not simple mapping</a>. */
-  @Test void testPushSimpleMappingProject() throws Exception {
+  @Test void testPushSimpleMappingProject() {
     final StringBuilder buf = new StringBuilder();
     final Table table = new BeatlesProjectableFilterableTable(buf, true);
     // Note that no redundant Project on EnumerableInterpreter
@@ -361,7 +365,7 @@ public class ScannableTableTest {
         + "EnumerableInterpreter\n"
         + "  BindableTableScan(table=[[s, beatles]], projects=[[2, 0]])";
     CalciteAssert.that()
-        .with(newSchema("s", Pair.of("beatles", table)))
+        .with(newSchema("s", PairList.of("beatles", table)))
         .query("select \"k\", \"i\" from \"s\".\"beatles\"")
         .explainContains(explain)
         .returnsUnordered(
@@ -377,7 +381,7 @@ public class ScannableTableTest {
    * Stack overflow error thrown when running join query</a>
    * Test two ProjectableFilterableTable can join and produce right plan.
    */
-  @Test void testProjectableFilterableTableJoin() throws Exception {
+  @Test void testProjectableFilterableTableJoin() {
     final StringBuilder buf = new StringBuilder();
     final String explain = "PLAN="
         + "EnumerableNestedLoopJoin(condition=[true], joinType=[inner])\n"
@@ -388,8 +392,12 @@ public class ScannableTableTest {
     CalciteAssert.that()
             .with(
               newSchema("s",
-                  Pair.of("b1", new BeatlesProjectableFilterableTable(buf, true)),
-                  Pair.of("b2", new BeatlesProjectableFilterableTable(buf, true))))
+                  PairList.<String, Table>builder()
+                      .add("b1",
+                          new BeatlesProjectableFilterableTable(buf, true))
+                      .add("b2",
+                          new BeatlesProjectableFilterableTable(buf, true))
+                      .build()))
             .query("select * from \"s\".\"b1\", \"s\".\"b2\" "
                     + "where \"s\".\"b1\".\"i\" = 10 and \"s\".\"b2\".\"i\" = 10 "
                     + "and \"s\".\"b1\".\"i\" = \"s\".\"b2\".\"i\"")
@@ -399,7 +407,7 @@ public class ScannableTableTest {
   /** Test case for
    * <a href="https://issues.apache.org/jira/browse/CALCITE-5019">[CALCITE-5019]
    * Avoid multiple scans when table is ProjectableFilterableTable</a>.*/
-  @Test void testProjectableFilterableWithScanCounter() throws Exception {
+  @Test void testProjectableFilterableWithScanCounter() {
     final StringBuilder buf = new StringBuilder();
     final BeatlesProjectableFilterableTable table =
         new BeatlesProjectableFilterableTable(buf, false);
@@ -407,7 +415,7 @@ public class ScannableTableTest {
         + "EnumerableInterpreter\n"
         + "  BindableTableScan(table=[[s, beatles]], filters=[[=($0, 4)]], projects=[[1]]";
     CalciteAssert.that()
-        .with(newSchema("s", Pair.of("beatles", table)))
+        .with(newSchema("s", PairList.of("beatles", table)))
         .query("select \"j\" from \"s\".\"beatles\" where \"i\" = 4")
         .explainContains(explain)
         .returnsUnordered("j=John", "j=Paul");
@@ -482,7 +490,7 @@ public class ScannableTableTest {
       final String explain = "PLAN="
           + "BindableTableScan(table=[[s, beatles]], filters=[[=($1, 'John')]], projects=[[1]])";
       CalciteAssert.that()
-          .with(newSchema("s", Pair.of("beatles", table)))
+          .with(newSchema("s", PairList.of("beatles", table)))
           .query("select \"j\" from \"s\".\"beatles\" where \"j\" = 'John'")
           .explainContains(explain)
           .returnsUnordered("j=John");
@@ -492,14 +500,12 @@ public class ScannableTableTest {
   }
 
   protected ConnectionPostProcessor newSchema(final String schemaName,
-      Pair<String, Table>... tables) {
+      PairList<String, Table> tables) {
     return connection -> {
       CalciteConnection con = connection.unwrap(CalciteConnection.class);
       SchemaPlus rootSchema = con.getRootSchema();
       SchemaPlus schema = rootSchema.add(schemaName, new AbstractSchema());
-      for (Pair<String, Table> t : tables) {
-        schema.add(t.left, t.right);
-      }
+      tables.forEach(schema::add);
       connection.setSchema(schemaName);
       return connection;
     };
@@ -531,6 +537,7 @@ public class ScannableTableTest {
       return typeFactory.builder()
           .add("i", SqlTypeName.INTEGER)
           .add("j", SqlTypeName.VARCHAR)
+          .add("k", SqlTypeName.INTEGER)
           .build();
     }
 
@@ -614,10 +621,10 @@ public class ScannableTableTest {
   private static Enumerator<Object[]> tens() {
     return new Enumerator<Object[]>() {
       int row = -1;
-      Object[] current;
+      Object @Nullable[] current;
 
       public Object[] current() {
-        return current;
+        return requireNonNull(current, "current");
       }
 
       public boolean moveNext() {
@@ -676,14 +683,15 @@ public class ScannableTableTest {
   };
 
   private static Enumerator<Object[]> beatles(final StringBuilder buf,
-      final Pair<Integer, Object> filter, final int[] projects) {
+      @Nullable final Pair<Integer, Object> filter,
+      final int @Nullable[] projects) {
     return new Enumerator<Object[]>() {
       int row = -1;
       int returnCount = 0;
-      Object[] current;
+      Object @Nullable[] current;
 
       public Object[] current() {
-        return current;
+        return requireNonNull(current, "current");
       }
 
       public boolean moveNext() {