You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@calcite.apache.org by vo...@apache.org on 2022/06/13 18:15:47 UTC

[calcite] branch main updated: [CALCITE-4448] Use TableMacro user-defined table functions with QueryableTable

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

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


The following commit(s) were added to refs/heads/main by this push:
     new d1cf4a38b [CALCITE-4448] Use TableMacro user-defined table functions with QueryableTable
d1cf4a38b is described below

commit d1cf4a38b71b514b1d208b5ffa3220b92693464f
Author: Volodymyr Vysotskyi <vv...@gmail.com>
AuthorDate: Sun Dec 27 23:01:49 2020 +0200

    [CALCITE-4448] Use TableMacro user-defined table functions with QueryableTable
---
 .../apache/calcite/prepare/RelOptTableImpl.java    |  79 ++++++-------
 .../java/org/apache/calcite/schema/Schemas.java    |  29 +++++
 .../apache/calcite/sql2rel/SqlToRelConverter.java  |  12 +-
 .../java/org/apache/calcite/test/JdbcTest.java     |   4 +-
 .../calcite/test/RelMdColumnOriginsTest.java       |   3 +-
 .../org/apache/calcite/test/TableFunctionTest.java |  22 ++++
 .../apache/calcite/test/TableInRootSchemaTest.java | 111 +----------------
 .../linq4j/tree/TableExpressionFactory.java        |  33 ++++++
 .../java/org/apache/calcite/piglet/PigTable.java   |   2 +-
 .../main/java/org/apache/calcite/util/Smalls.java  | 131 +++++++++++++++++++++
 10 files changed, 269 insertions(+), 157 deletions(-)

diff --git a/core/src/main/java/org/apache/calcite/prepare/RelOptTableImpl.java b/core/src/main/java/org/apache/calcite/prepare/RelOptTableImpl.java
index 087829249..29a6f07c0 100644
--- a/core/src/main/java/org/apache/calcite/prepare/RelOptTableImpl.java
+++ b/core/src/main/java/org/apache/calcite/prepare/RelOptTableImpl.java
@@ -18,6 +18,7 @@ package org.apache.calcite.prepare;
 
 import org.apache.calcite.jdbc.CalciteSchema;
 import org.apache.calcite.linq4j.tree.Expression;
+import org.apache.calcite.linq4j.tree.TableExpressionFactory;
 import org.apache.calcite.materialize.Lattice;
 import org.apache.calcite.plan.RelOptSchema;
 import org.apache.calcite.plan.RelOptTable;
@@ -34,11 +35,8 @@ import org.apache.calcite.rel.type.RelDataTypeField;
 import org.apache.calcite.rel.type.RelProtoDataType;
 import org.apache.calcite.rel.type.RelRecordType;
 import org.apache.calcite.schema.ColumnStrategy;
-import org.apache.calcite.schema.FilterableTable;
 import org.apache.calcite.schema.ModifiableTable;
 import org.apache.calcite.schema.Path;
-import org.apache.calcite.schema.ProjectableFilterableTable;
-import org.apache.calcite.schema.QueryableTable;
 import org.apache.calcite.schema.ScannableTable;
 import org.apache.calcite.schema.Schema;
 import org.apache.calcite.schema.SchemaPlus;
@@ -66,7 +64,6 @@ import java.util.AbstractList;
 import java.util.Collection;
 import java.util.List;
 import java.util.Set;
-import java.util.function.Function;
 
 import static java.util.Objects.requireNonNull;
 
@@ -77,7 +74,7 @@ public class RelOptTableImpl extends Prepare.AbstractPreparingTable {
   private final @Nullable RelOptSchema schema;
   private final RelDataType rowType;
   private final @Nullable Table table;
-  private final @Nullable Function<Class, Expression> expressionFunction;
+  private final @Nullable TableExpressionFactory tableExpressionFactory;
   private final ImmutableList<String> names;
 
   /** Estimate for the row count, or null.
@@ -94,13 +91,13 @@ public class RelOptTableImpl extends Prepare.AbstractPreparingTable {
       RelDataType rowType,
       List<String> names,
       @Nullable Table table,
-      @Nullable Function<Class, Expression> expressionFunction,
+      @Nullable TableExpressionFactory tableExpressionFactory,
       @Nullable Double rowCount) {
     this.schema = schema;
     this.rowType = requireNonNull(rowType, "rowType");
     this.names = ImmutableList.copyOf(names);
     this.table = table; // may be null
-    this.expressionFunction = expressionFunction; // may be null
+    this.tableExpressionFactory = tableExpressionFactory; // may be null
     this.rowCount = rowCount; // may be null
   }
 
@@ -113,29 +110,53 @@ public class RelOptTableImpl extends Prepare.AbstractPreparingTable {
         c -> expression, null);
   }
 
+  @Deprecated // to be removed before 2.0
   public static RelOptTableImpl create(
       @Nullable RelOptSchema schema,
       RelDataType rowType,
       List<String> names,
       Table table,
       Expression expression) {
+    return create(schema, rowType, names, table, c -> expression);
+  }
+
+  /**
+   * Creates {@link RelOptTableImpl} instance with specified arguments
+   * and row count obtained from table statistic.
+   *
+   * @param schema table schema
+   * @param rowType table row type
+   * @param names full table path
+   * @param table table
+   * @param expressionFactory expression function for accessing table data
+   *                          in the generated code
+   *
+   * @return {@link RelOptTableImpl} instance
+   */
+  public static RelOptTableImpl create(
+      @Nullable RelOptSchema schema,
+      RelDataType rowType,
+      List<String> names,
+      Table table,
+      TableExpressionFactory expressionFactory) {
     return new RelOptTableImpl(schema, rowType, names, table,
-        c -> expression, table.getStatistic().getRowCount());
+        expressionFactory, table.getStatistic().getRowCount());
   }
 
   public static RelOptTableImpl create(@Nullable RelOptSchema schema, RelDataType rowType,
       Table table, Path path) {
     final SchemaPlus schemaPlus = MySchemaPlus.create(path);
     return new RelOptTableImpl(schema, rowType, Pair.left(path), table,
-        getClassExpressionFunction(schemaPlus, Util.last(path).left, table),
+        c -> Schemas.getTableExpression(schemaPlus, Util.last(path).left, table, c),
         table.getStatistic().getRowCount());
   }
 
   public static RelOptTableImpl create(@Nullable RelOptSchema schema, RelDataType rowType,
       final CalciteSchema.TableEntry tableEntry, @Nullable Double rowCount) {
     final Table table = tableEntry.getTable();
-    return new RelOptTableImpl(schema, rowType, tableEntry.path(),
-        table, getClassExpressionFunction(tableEntry, table), rowCount);
+    return new RelOptTableImpl(schema, rowType, tableEntry.path(), table,
+        c -> Schemas.getTableExpression(tableEntry.schema.plus(), tableEntry.name, table, c),
+        rowCount);
   }
 
   /**
@@ -143,7 +164,7 @@ public class RelOptTableImpl extends Prepare.AbstractPreparingTable {
    */
   public RelOptTableImpl copy(RelDataType newRowType) {
     return new RelOptTableImpl(this.schema, newRowType, this.names, this.table,
-        this.expressionFunction, this.rowCount);
+        this.tableExpressionFactory, this.rowCount);
   }
 
   @Override public String toString() {
@@ -155,32 +176,6 @@ public class RelOptTableImpl extends Prepare.AbstractPreparingTable {
         + '}';
   }
 
-  private static Function<Class, Expression> getClassExpressionFunction(
-      CalciteSchema.TableEntry tableEntry, Table table) {
-    return getClassExpressionFunction(tableEntry.schema.plus(), tableEntry.name,
-        table);
-  }
-
-  private static Function<Class, Expression> getClassExpressionFunction(
-      final SchemaPlus schema, final String tableName, final Table table) {
-    if (table instanceof QueryableTable) {
-      final QueryableTable queryableTable = (QueryableTable) table;
-      return clazz -> queryableTable.getExpression(schema, tableName, clazz);
-    } else if (table instanceof ScannableTable
-        || table instanceof FilterableTable
-        || table instanceof ProjectableFilterableTable) {
-      return clazz -> Schemas.tableExpression(schema, Object[].class, tableName,
-          table.getClass());
-    } else if (table instanceof StreamableTable) {
-      return getClassExpressionFunction(schema, tableName,
-          ((StreamableTable) table).stream());
-    } else {
-      return input -> {
-        throw new UnsupportedOperationException();
-      };
-    }
-  }
-
   public static RelOptTableImpl create(@Nullable RelOptSchema schema,
       RelDataType rowType, Table table, ImmutableList<String> names) {
     assert table instanceof TranslatableTable
@@ -211,10 +206,10 @@ public class RelOptTableImpl extends Prepare.AbstractPreparingTable {
   }
 
   @Override public @Nullable Expression getExpression(Class clazz) {
-    if (expressionFunction == null) {
+    if (tableExpressionFactory == null) {
       return null;
     }
-    return expressionFunction.apply(clazz);
+    return tableExpressionFactory.create(clazz);
   }
 
   @Override protected RelOptTable extend(Table extendedTable) {
@@ -222,7 +217,7 @@ public class RelOptTableImpl extends Prepare.AbstractPreparingTable {
     final RelDataType extendedRowType =
         extendedTable.getRowType(schema.getTypeFactory());
     return new RelOptTableImpl(schema, extendedRowType, getQualifiedName(),
-        extendedTable, expressionFunction, getRowCount());
+        extendedTable, tableExpressionFactory, getRowCount());
   }
 
   @Override public boolean equals(@Nullable Object obj) {
@@ -274,7 +269,7 @@ public class RelOptTableImpl extends Prepare.AbstractPreparingTable {
       }
       final RelOptTable relOptTable =
           new RelOptTableImpl(this.schema, b.build(), this.names, this.table,
-              this.expressionFunction, this.rowCount) {
+              this.tableExpressionFactory, this.rowCount) {
             @Override public <T extends Object> @Nullable T unwrap(Class<T> clazz) {
               if (clazz.isAssignableFrom(InitializerExpressionFactory.class)) {
                 return clazz.cast(NullInitializerExpressionFactory.INSTANCE);
diff --git a/core/src/main/java/org/apache/calcite/schema/Schemas.java b/core/src/main/java/org/apache/calcite/schema/Schemas.java
index 54a82f1b8..634abda7f 100644
--- a/core/src/main/java/org/apache/calcite/schema/Schemas.java
+++ b/core/src/main/java/org/apache/calcite/schema/Schemas.java
@@ -184,6 +184,35 @@ public final class Schemas {
     return EnumUtils.convert(expression, clazz);
   }
 
+  /**
+   * Generates an expression with which table can be referenced in
+   * generated code.
+   *
+   * @param schema    Schema
+   * @param tableName Table name (unique within schema)
+   * @param table     Table to be referenced
+   * @param clazz     Class that provides specific methods for accessing table data.
+   *                  It may differ from the {@code table} class; for example {@code clazz} may be
+   *                  {@code MongoTable.MongoQueryable}, though {@code table} is {@code MongoTable}
+   */
+  public static Expression getTableExpression(SchemaPlus schema, String tableName,
+      Table table, Class<?> clazz) {
+    if (table instanceof QueryableTable) {
+      QueryableTable queryableTable = (QueryableTable) table;
+      return queryableTable.getExpression(schema, tableName, clazz);
+    } else if (table instanceof ScannableTable
+        || table instanceof FilterableTable
+        || table instanceof ProjectableFilterableTable) {
+      return tableExpression(schema, Object[].class, tableName,
+          table.getClass());
+    } else if (table instanceof StreamableTable) {
+      return getTableExpression(schema, tableName,
+          ((StreamableTable) table).stream(), clazz);
+    } else {
+      throw new UnsupportedOperationException();
+    }
+  }
+
   public static DataContext createDataContext(
       Connection connection, @Nullable SchemaPlus rootSchema) {
     return DataContexts.of((CalciteConnection) connection, rootSchema);
diff --git a/core/src/main/java/org/apache/calcite/sql2rel/SqlToRelConverter.java b/core/src/main/java/org/apache/calcite/sql2rel/SqlToRelConverter.java
index adc19e5d2..166b593f1 100644
--- a/core/src/main/java/org/apache/calcite/sql2rel/SqlToRelConverter.java
+++ b/core/src/main/java/org/apache/calcite/sql2rel/SqlToRelConverter.java
@@ -17,7 +17,9 @@
 package org.apache.calcite.sql2rel;
 
 import org.apache.calcite.avatica.util.Spaces;
+import org.apache.calcite.jdbc.CalciteSchema;
 import org.apache.calcite.linq4j.Ord;
+import org.apache.calcite.linq4j.tree.TableExpressionFactory;
 import org.apache.calcite.plan.RelOptCluster;
 import org.apache.calcite.plan.RelOptPlanner;
 import org.apache.calcite.plan.RelOptSamplingParameters;
@@ -90,6 +92,7 @@ import org.apache.calcite.rex.RexWindowBounds;
 import org.apache.calcite.schema.ColumnStrategy;
 import org.apache.calcite.schema.ModifiableTable;
 import org.apache.calcite.schema.ModifiableView;
+import org.apache.calcite.schema.Schemas;
 import org.apache.calcite.schema.Table;
 import org.apache.calcite.schema.TranslatableTable;
 import org.apache.calcite.schema.Wrapper;
@@ -2667,8 +2670,13 @@ public class SqlToRelConverter {
           (SqlUserDefinedTableMacro) operator;
       final TranslatableTable table = udf.getTable(callBinding);
       final RelDataType rowType = table.getRowType(typeFactory);
-      RelOptTable relOptTable = RelOptTableImpl.create(null, rowType, table,
-          udf.getNameAsId().names);
+      CalciteSchema schema = Schemas.subSchema(
+          catalogReader.getRootSchema(), udf.getNameAsId().skipLast(1).names);
+      TableExpressionFactory expressionFunction =
+          clazz -> Schemas.getTableExpression(Objects.requireNonNull(schema, "schema").plus(),
+              Util.last(udf.getNameAsId().names), table, clazz);
+      RelOptTable relOptTable = RelOptTableImpl.create(null, rowType,
+          udf.getNameAsId().names, table, expressionFunction);
       RelNode converted = toRel(relOptTable, ImmutableList.of());
       bb.setRoot(converted, true);
       return;
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 2ae92cb1b..0725666ef 100644
--- a/core/src/test/java/org/apache/calcite/test/JdbcTest.java
+++ b/core/src/test/java/org/apache/calcite/test/JdbcTest.java
@@ -7262,8 +7262,8 @@ public class JdbcTest {
     }
 
     // add tables and retrieve with various case sensitivities
-    final TableInRootSchemaTest.SimpleTable table =
-        new TableInRootSchemaTest.SimpleTable();
+    final Smalls.SimpleTable table =
+        new Smalls.SimpleTable();
     a2Schema.add("table1", table);
     a2Schema.add("TABLE1", table);
     a2Schema.add("tabLe1", table);
diff --git a/core/src/test/java/org/apache/calcite/test/RelMdColumnOriginsTest.java b/core/src/test/java/org/apache/calcite/test/RelMdColumnOriginsTest.java
index 4f19ebc9b..d034e9906 100644
--- a/core/src/test/java/org/apache/calcite/test/RelMdColumnOriginsTest.java
+++ b/core/src/test/java/org/apache/calcite/test/RelMdColumnOriginsTest.java
@@ -17,6 +17,7 @@
 package org.apache.calcite.test;
 
 import org.apache.calcite.jdbc.CalciteConnection;
+import org.apache.calcite.util.Smalls;
 
 import com.google.common.collect.ImmutableMultiset;
 
@@ -43,7 +44,7 @@ class RelMdColumnOriginsTest {
         connection.unwrap(CalciteConnection.class);
 
     calciteConnection.getRootSchema().add("T1",
-        new TableInRootSchemaTest.SimpleTable());
+        new Smalls.SimpleTable());
     Statement statement = calciteConnection.createStatement();
     ResultSet resultSet =
         statement.executeQuery("SELECT TABLE1.ID, TABLE2.ID FROM "
diff --git a/core/src/test/java/org/apache/calcite/test/TableFunctionTest.java b/core/src/test/java/org/apache/calcite/test/TableFunctionTest.java
index 36d32295f..ab50be530 100644
--- a/core/src/test/java/org/apache/calcite/test/TableFunctionTest.java
+++ b/core/src/test/java/org/apache/calcite/test/TableFunctionTest.java
@@ -547,4 +547,26 @@ class TableFunctionTest {
       assertThat(CalciteAssert.toString(resultSet), equalTo(expected));
     }
   }
+
+  /** Test case for
+   * <a href="https://issues.apache.org/jira/browse/CALCITE-4448">[CALCITE-4448]
+   * Use TableMacro user-defined table functions with QueryableTable</a>. */
+  @Test void testQueryableTableWithTableMacro() throws SQLException {
+    try (Connection connection =
+        DriverManager.getConnection("jdbc:calcite:")) {
+      CalciteConnection calciteConnection =
+          connection.unwrap(CalciteConnection.class);
+      SchemaPlus rootSchema = calciteConnection.getRootSchema();
+      SchemaPlus schema = rootSchema.add("s", new AbstractSchema());
+      schema.add("simple", new Smalls.SimpleTableMacro());
+
+      String sql = "select * from table(\"s\".\"simple\"())";
+      ResultSet resultSet = connection.createStatement().executeQuery(sql);
+      String expected = "A=foo; B=5\n"
+          + "A=bar; B=4\n"
+          + "A=foo; B=3\n";
+      assertThat(CalciteAssert.toString(resultSet),
+          equalTo(expected));
+    }
+  }
 }
diff --git a/core/src/test/java/org/apache/calcite/test/TableInRootSchemaTest.java b/core/src/test/java/org/apache/calcite/test/TableInRootSchemaTest.java
index 5127fd87c..55b058a2f 100644
--- a/core/src/test/java/org/apache/calcite/test/TableInRootSchemaTest.java
+++ b/core/src/test/java/org/apache/calcite/test/TableInRootSchemaTest.java
@@ -16,21 +16,8 @@
  */
 package org.apache.calcite.test;
 
-import org.apache.calcite.adapter.enumerable.EnumerableTableScan;
-import org.apache.calcite.adapter.java.AbstractQueryableTable;
 import org.apache.calcite.jdbc.CalciteConnection;
-import org.apache.calcite.linq4j.Enumerator;
-import org.apache.calcite.linq4j.Linq4j;
-import org.apache.calcite.linq4j.QueryProvider;
-import org.apache.calcite.linq4j.Queryable;
-import org.apache.calcite.plan.RelOptTable;
-import org.apache.calcite.rel.RelNode;
-import org.apache.calcite.rel.type.RelDataType;
-import org.apache.calcite.rel.type.RelDataTypeFactory;
-import org.apache.calcite.schema.SchemaPlus;
-import org.apache.calcite.schema.TranslatableTable;
-import org.apache.calcite.schema.impl.AbstractTableQueryable;
-import org.apache.calcite.util.Pair;
+import org.apache.calcite.util.Smalls;
 
 import com.google.common.collect.ImmutableMultiset;
 
@@ -41,10 +28,6 @@ import java.sql.DriverManager;
 import java.sql.ResultSet;
 import java.sql.ResultSetMetaData;
 import java.sql.Statement;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Iterator;
-import java.util.List;
 
 import static org.hamcrest.CoreMatchers.equalTo;
 import static org.hamcrest.CoreMatchers.nullValue;
@@ -61,7 +44,7 @@ class TableInRootSchemaTest {
     CalciteConnection calciteConnection =
         connection.unwrap(CalciteConnection.class);
 
-    calciteConnection.getRootSchema().add("SAMPLE", new SimpleTable());
+    calciteConnection.getRootSchema().add("SAMPLE", new Smalls.SimpleTable());
     Statement statement = calciteConnection.createStatement();
     ResultSet resultSet =
         statement.executeQuery("select A, SUM(B) from SAMPLE group by A");
@@ -90,94 +73,4 @@ class TableInRootSchemaTest {
     connection.close();
   }
 
-  /** Table with columns (A, B). */
-  public static class SimpleTable extends AbstractQueryableTable
-      implements TranslatableTable {
-    private String[] columnNames = { "A", "B" };
-    private Class[] columnTypes = { String.class, Integer.class };
-    private Object[][] rows = new Object[3][];
-
-    SimpleTable() {
-      super(Object[].class);
-
-      rows[0] = new Object[] { "foo", 5 };
-      rows[1] = new Object[] { "bar", 4 };
-      rows[2] = new Object[] { "foo", 3 };
-    }
-
-    public RelDataType getRowType(RelDataTypeFactory typeFactory) {
-      int columnCount = columnNames.length;
-      final List<Pair<String, RelDataType>> columnDesc =
-          new ArrayList<>(columnCount);
-      for (int i = 0; i < columnCount; i++) {
-        final RelDataType colType = typeFactory
-            .createJavaType(columnTypes[i]);
-        columnDesc.add(Pair.of(columnNames[i], colType));
-      }
-      return typeFactory.createStructType(columnDesc);
-    }
-
-    public Iterator<Object[]> iterator() {
-      return Linq4j.enumeratorIterator(enumerator());
-    }
-
-    public Enumerator<Object[]> enumerator() {
-      return enumeratorImpl(null);
-    }
-
-    public <T> Queryable<T> asQueryable(QueryProvider queryProvider,
-        SchemaPlus schema, String tableName) {
-      return new AbstractTableQueryable<T>(queryProvider, schema, this,
-          tableName) {
-        public Enumerator<T> enumerator() {
-          //noinspection unchecked
-          return (Enumerator<T>) enumeratorImpl(null);
-        }
-      };
-    }
-
-    private Enumerator<Object[]> enumeratorImpl(final int[] fields) {
-      return new Enumerator<Object[]>() {
-        private Object[] current;
-        private Iterator<Object[]> iterator = Arrays.asList(rows)
-            .iterator();
-
-        public Object[] current() {
-          return current;
-        }
-
-        public boolean moveNext() {
-          if (iterator.hasNext()) {
-            Object[] full = iterator.next();
-            current = fields != null ? convertRow(full) : full;
-            return true;
-          } else {
-            current = null;
-            return false;
-          }
-        }
-
-        public void reset() {
-          throw new UnsupportedOperationException();
-        }
-
-        public void close() {
-          // noop
-        }
-
-        private Object[] convertRow(Object[] full) {
-          final Object[] objects = new Object[fields.length];
-          for (int i = 0; i < fields.length; i++) {
-            objects[i] = full[fields[i]];
-          }
-          return objects;
-        }
-      };
-    }
-
-    public RelNode toRel(RelOptTable.ToRelContext context,
-        RelOptTable relOptTable) {
-      return EnumerableTableScan.create(context.getCluster(), relOptTable);
-    }
-  }
 }
diff --git a/linq4j/src/main/java/org/apache/calcite/linq4j/tree/TableExpressionFactory.java b/linq4j/src/main/java/org/apache/calcite/linq4j/tree/TableExpressionFactory.java
new file mode 100644
index 000000000..e3ea73da8
--- /dev/null
+++ b/linq4j/src/main/java/org/apache/calcite/linq4j/tree/TableExpressionFactory.java
@@ -0,0 +1,33 @@
+/*
+ * 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.linq4j.tree;
+
+/**
+ * Factory for creating table expressions that may be used in generated code
+ * for accessing table data.
+ */
+public interface TableExpressionFactory {
+
+  /**
+   * Creates {@link Expression} to be used in generated code for accessing table data.
+   *
+   * @param clazz Class that provides specific methods for accessing table data.
+   *
+   * @return {@link Expression} instance
+   */
+  Expression create(Class clazz);
+}
diff --git a/piglet/src/main/java/org/apache/calcite/piglet/PigTable.java b/piglet/src/main/java/org/apache/calcite/piglet/PigTable.java
index 0ab303a5a..94a8f7427 100644
--- a/piglet/src/main/java/org/apache/calcite/piglet/PigTable.java
+++ b/piglet/src/main/java/org/apache/calcite/piglet/PigTable.java
@@ -59,7 +59,7 @@ public class PigTable extends AbstractTable implements ScannableTable {
       RelDataType rowType, List<String> names) {
     final PigTable pigTable = new PigTable(rowType);
     return RelOptTableImpl.create(schema, rowType, names, pigTable,
-        Expressions.constant(Boolean.TRUE));
+        c -> Expressions.constant(Boolean.TRUE));
   }
 
   @Override public RelDataType getRowType(final RelDataTypeFactory typeFactory) {
diff --git a/testkit/src/main/java/org/apache/calcite/util/Smalls.java b/testkit/src/main/java/org/apache/calcite/util/Smalls.java
index fefa32491..deba6b010 100644
--- a/testkit/src/main/java/org/apache/calcite/util/Smalls.java
+++ b/testkit/src/main/java/org/apache/calcite/util/Smalls.java
@@ -17,6 +17,7 @@
 package org.apache.calcite.util;
 
 import org.apache.calcite.DataContext;
+import org.apache.calcite.adapter.enumerable.EnumerableTableScan;
 import org.apache.calcite.adapter.java.AbstractQueryableTable;
 import org.apache.calcite.config.CalciteConnectionConfig;
 import org.apache.calcite.linq4j.AbstractEnumerable;
@@ -29,21 +30,30 @@ import org.apache.calcite.linq4j.Queryable;
 import org.apache.calcite.linq4j.function.Deterministic;
 import org.apache.calcite.linq4j.function.Parameter;
 import org.apache.calcite.linq4j.function.SemiStrict;
+import org.apache.calcite.linq4j.tree.Expression;
+import org.apache.calcite.linq4j.tree.Expressions;
+import org.apache.calcite.linq4j.tree.MethodCallExpression;
 import org.apache.calcite.linq4j.tree.Types;
+import org.apache.calcite.plan.RelOptTable;
+import org.apache.calcite.rel.RelNode;
 import org.apache.calcite.rel.externalize.RelJsonReader;
 import org.apache.calcite.rel.type.RelDataType;
 import org.apache.calcite.rel.type.RelDataTypeFactory;
 import org.apache.calcite.rex.RexLiteral;
 import org.apache.calcite.runtime.SqlFunctions;
 import org.apache.calcite.schema.FunctionContext;
+import org.apache.calcite.schema.FunctionParameter;
 import org.apache.calcite.schema.QueryableTable;
 import org.apache.calcite.schema.ScannableTable;
 import org.apache.calcite.schema.Schema;
 import org.apache.calcite.schema.SchemaPlus;
+import org.apache.calcite.schema.Schemas;
 import org.apache.calcite.schema.Statistic;
 import org.apache.calcite.schema.Statistics;
+import org.apache.calcite.schema.TableMacro;
 import org.apache.calcite.schema.TranslatableTable;
 import org.apache.calcite.schema.impl.AbstractTable;
+import org.apache.calcite.schema.impl.AbstractTableQueryable;
 import org.apache.calcite.schema.impl.ViewTable;
 import org.apache.calcite.sql.SqlCall;
 import org.apache.calcite.sql.SqlNode;
@@ -61,7 +71,10 @@ import java.sql.Date;
 import java.sql.Time;
 import java.sql.Timestamp;
 import java.util.AbstractList;
+import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Collections;
+import java.util.Iterator;
 import java.util.List;
 import java.util.Locale;
 import java.util.Map;
@@ -1313,4 +1326,122 @@ public class Smalls {
       this.sale0 = sale;
     }
   }
+
+  /**
+   * Implementation of {@link TableMacro} interface with
+   * {@link #apply} method that returns {@link Queryable} table.
+   */
+  public static class SimpleTableMacro implements TableMacro {
+
+    @Override public TranslatableTable apply(List<?> arguments) {
+      return new SimpleTable();
+    }
+
+    @Override public List<FunctionParameter> getParameters() {
+      return Collections.emptyList();
+    }
+  }
+
+  /** Table with columns (A, B). */
+  public static class SimpleTable extends AbstractQueryableTable
+      implements TranslatableTable {
+    private final String[] columnNames = { "A", "B" };
+    private final Class<?>[] columnTypes = { String.class, Integer.class };
+    private final Object[][] rows = new Object[3][];
+
+    public SimpleTable() {
+      super(Object[].class);
+
+      rows[0] = new Object[] { "foo", 5 };
+      rows[1] = new Object[] { "bar", 4 };
+      rows[2] = new Object[] { "foo", 3 };
+    }
+
+    @Override public RelDataType getRowType(RelDataTypeFactory typeFactory) {
+      int columnCount = columnNames.length;
+      final List<Pair<String, RelDataType>> columnDesc =
+          new ArrayList<>(columnCount);
+      for (int i = 0; i < columnCount; i++) {
+        final RelDataType colType = typeFactory
+            .createJavaType(columnTypes[i]);
+        columnDesc.add(Pair.of(columnNames[i], colType));
+      }
+      return typeFactory.createStructType(columnDesc);
+    }
+
+    public Iterator<Object[]> iterator() {
+      return Linq4j.enumeratorIterator(enumerator());
+    }
+
+    public Enumerator<Object[]> enumerator() {
+      return enumeratorImpl(null);
+    }
+
+    @Override public <T> Queryable<T> asQueryable(QueryProvider queryProvider,
+        SchemaPlus schema, String tableName) {
+      return new AbstractTableQueryable<T>(queryProvider, schema, this,
+          tableName) {
+        @Override public Enumerator<T> enumerator() {
+          //noinspection unchecked
+          return (Enumerator<T>) enumeratorImpl(null);
+        }
+      };
+    }
+
+    private Enumerator<Object[]> enumeratorImpl(final int[] fields) {
+      return new Enumerator<Object[]>() {
+        private Object[] current;
+        private final Iterator<Object[]> iterator = Arrays.asList(rows)
+            .iterator();
+
+        @Override public Object[] current() {
+          return current;
+        }
+
+        @Override public boolean moveNext() {
+          if (iterator.hasNext()) {
+            Object[] full = iterator.next();
+            current = fields != null ? convertRow(full) : full;
+            return true;
+          } else {
+            current = null;
+            return false;
+          }
+        }
+
+        @Override public void reset() {
+          throw new UnsupportedOperationException();
+        }
+
+        @Override public void close() {
+          // noop
+        }
+
+        private Object[] convertRow(Object[] full) {
+          final Object[] objects = new Object[fields.length];
+          for (int i = 0; i < fields.length; i++) {
+            objects[i] = full[fields[i]];
+          }
+          return objects;
+        }
+      };
+    }
+
+    @Override public RelNode toRel(
+        RelOptTable.ToRelContext context,
+        RelOptTable relOptTable) {
+      return EnumerableTableScan.create(context.getCluster(), relOptTable);
+    }
+
+    @Override public Expression getExpression(SchemaPlus schema, String tableName, Class clazz) {
+      MethodCallExpression queryableExpression =
+          Expressions.call(Expressions.new_(SimpleTable.class),
+              BuiltInMethod.QUERYABLE_TABLE_AS_QUERYABLE.method,
+              Expressions.constant(null),
+              Schemas.expression(schema),
+              Expressions.constant(tableName));
+      return Expressions.call(queryableExpression,
+          BuiltInMethod.QUERYABLE_AS_ENUMERABLE.method);
+    }
+  }
 }