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 2021/03/13 07:11:39 UTC

[calcite] 04/04: [CALCITE-4477] In Interpreter, support table-valued functions

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

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

commit 084d608c6adbbb82bcbdc2778439dfbeb6d6afdd
Author: Julian Hyde <jh...@apache.org>
AuthorDate: Mon Jan 25 00:10:08 2021 -0800

    [CALCITE-4477] In Interpreter, support table-valued functions
    
    Fix RexCallBinding, so that we can access constant arguments
    to table function scans when created via RelBuilder.
    
    Add Fibonacci table function, as a test.
---
 .../java/org/apache/calcite/interpreter/Nodes.java |  5 ++
 .../calcite/interpreter/TableFunctionScanNode.java | 75 ++++++++++++++++
 .../calcite/schema/impl/TableFunctionImpl.java     | 14 +--
 .../org/apache/calcite/sql/SqlCallBinding.java     | 18 ----
 .../org/apache/calcite/sql/SqlOperatorBinding.java | 18 +++-
 .../org/apache/calcite/test/InterpreterTest.java   | 51 +++++++++++
 .../apache/calcite/test/MockSqlOperatorTable.java  | 19 +++++
 .../org/apache/calcite/test/RelBuilderTest.java    | 99 ++++++++++++++++++++++
 .../org/apache/calcite/test/TableFunctionTest.java |  2 +-
 .../test/java/org/apache/calcite/util/Smalls.java  | 64 +++++++++++++-
 10 files changed, 336 insertions(+), 29 deletions(-)

diff --git a/core/src/main/java/org/apache/calcite/interpreter/Nodes.java b/core/src/main/java/org/apache/calcite/interpreter/Nodes.java
index 84224d0..511eeaa 100644
--- a/core/src/main/java/org/apache/calcite/interpreter/Nodes.java
+++ b/core/src/main/java/org/apache/calcite/interpreter/Nodes.java
@@ -25,6 +25,7 @@ import org.apache.calcite.rel.core.Match;
 import org.apache.calcite.rel.core.Project;
 import org.apache.calcite.rel.core.SetOp;
 import org.apache.calcite.rel.core.Sort;
+import org.apache.calcite.rel.core.TableFunctionScan;
 import org.apache.calcite.rel.core.TableScan;
 import org.apache.calcite.rel.core.Uncollect;
 import org.apache.calcite.rel.core.Values;
@@ -74,6 +75,10 @@ public class Nodes {
       node = TableScanNode.create(this, scan, scan.filters, scan.projects);
     }
 
+    public void visit(TableFunctionScan functionScan) {
+      node = TableFunctionScanNode.create(this, functionScan);
+    }
+
     public void visit(Sort sort) {
       node = new SortNode(this, sort);
     }
diff --git a/core/src/main/java/org/apache/calcite/interpreter/TableFunctionScanNode.java b/core/src/main/java/org/apache/calcite/interpreter/TableFunctionScanNode.java
new file mode 100644
index 0000000..395cd57
--- /dev/null
+++ b/core/src/main/java/org/apache/calcite/interpreter/TableFunctionScanNode.java
@@ -0,0 +1,75 @@
+/*
+ * 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.interpreter;
+
+import org.apache.calcite.linq4j.Enumerable;
+import org.apache.calcite.linq4j.Enumerator;
+import org.apache.calcite.rel.core.TableFunctionScan;
+import org.apache.calcite.rex.RexCall;
+import org.apache.calcite.rex.RexNode;
+import org.apache.calcite.runtime.Enumerables;
+import org.apache.calcite.schema.Function;
+import org.apache.calcite.schema.impl.TableFunctionImpl;
+import org.apache.calcite.sql.SqlOperator;
+import org.apache.calcite.sql.validate.SqlUserDefinedTableFunction;
+
+import com.google.common.collect.ImmutableList;
+
+/**
+ * Interpreter node that implements a
+ * {@link TableFunctionScan}.
+ */
+public class TableFunctionScanNode implements Node {
+  private final Scalar scalar;
+  private final Context context;
+  private final Sink sink;
+
+  private TableFunctionScanNode(Compiler compiler, TableFunctionScan rel) {
+    this.scalar =
+        compiler.compile(ImmutableList.of(rel.getCall()), rel.getRowType());
+    this.context = compiler.createContext();
+    this.sink = compiler.sink(rel);
+  }
+
+  @Override public void run() throws InterruptedException {
+    final Object o = scalar.execute(context);
+    if (o instanceof Enumerable) {
+      @SuppressWarnings("unchecked") final Enumerable<Row> rowEnumerable =
+          Enumerables.toRow((Enumerable) o);
+      final Enumerator<Row> enumerator = rowEnumerable.enumerator();
+      while (enumerator.moveNext()) {
+        sink.send(enumerator.current());
+      }
+    }
+  }
+
+  /** Creates a TableFunctionScanNode. */
+  static TableFunctionScanNode create(Compiler compiler, TableFunctionScan rel) {
+    RexNode call = rel.getCall();
+    if (call instanceof RexCall) {
+      SqlOperator operator = ((RexCall) call).getOperator();
+      if (operator instanceof SqlUserDefinedTableFunction) {
+        Function function = ((SqlUserDefinedTableFunction) operator).function;
+        if (function instanceof TableFunctionImpl) {
+          return new TableFunctionScanNode(compiler, rel);
+        }
+      }
+    }
+    throw new AssertionError("cannot convert table function scan "
+        + rel.getCall() + " to enumerable");
+  }
+}
diff --git a/core/src/main/java/org/apache/calcite/schema/impl/TableFunctionImpl.java b/core/src/main/java/org/apache/calcite/schema/impl/TableFunctionImpl.java
index 36d4edf..fa4d699 100644
--- a/core/src/main/java/org/apache/calcite/schema/impl/TableFunctionImpl.java
+++ b/core/src/main/java/org/apache/calcite/schema/impl/TableFunctionImpl.java
@@ -16,7 +16,6 @@
  */
 package org.apache.calcite.schema.impl;
 
-import org.apache.calcite.DataContext;
 import org.apache.calcite.adapter.enumerable.CallImplementor;
 import org.apache.calcite.adapter.enumerable.NullPolicy;
 import org.apache.calcite.adapter.enumerable.ReflectiveCallNotNullImplementor;
@@ -53,8 +52,8 @@ import static java.util.Objects.requireNonNull;
  * Implementation of {@link org.apache.calcite.schema.TableFunction} based on a
  * method.
 */
-public class TableFunctionImpl extends ReflectiveFunctionBase implements
-    TableFunction, ImplementableFunction {
+public class TableFunctionImpl extends ReflectiveFunctionBase
+    implements TableFunction, ImplementableFunction {
   private final CallImplementor implementor;
 
   /** Private constructor; use {@link #create}. */
@@ -129,7 +128,7 @@ public class TableFunctionImpl extends ReflectiveFunctionBase implements
               Expression queryable = Expressions.call(
                   Expressions.convert_(expr, QueryableTable.class),
                   BuiltInMethod.QUERYABLE_TABLE_AS_QUERYABLE.method,
-                  Expressions.call(DataContext.ROOT,
+                  Expressions.call(translator.getRoot(),
                       BuiltInMethod.DATA_CONTEXT_GET_QUERY_PROVIDER.method),
                   Expressions.constant(null, SchemaPlus.class),
                   Expressions.constant(call.getOperator().getName(), String.class));
@@ -137,7 +136,8 @@ public class TableFunctionImpl extends ReflectiveFunctionBase implements
                   BuiltInMethod.QUERYABLE_AS_ENUMERABLE.method);
             } else {
               expr = Expressions.call(expr,
-                  BuiltInMethod.SCANNABLE_TABLE_SCAN.method, DataContext.ROOT);
+                  BuiltInMethod.SCANNABLE_TABLE_SCAN.method,
+                  translator.getRoot());
             }
             return expr;
           }
@@ -152,8 +152,8 @@ public class TableFunctionImpl extends ReflectiveFunctionBase implements
             method.getDeclaringClass().getConstructor();
         o = constructor.newInstance();
       }
-      return (Table) requireNonNull(
-          method.invoke(o, arguments.toArray()),
+      final Object table = method.invoke(o, arguments.toArray());
+      return (Table) requireNonNull(table,
           () -> "got null from " + method + " with arguments " + arguments);
     } catch (IllegalArgumentException e) {
       throw RESOURCE.illegalArgumentForTableFunctionCall(
diff --git a/core/src/main/java/org/apache/calcite/sql/SqlCallBinding.java b/core/src/main/java/org/apache/calcite/sql/SqlCallBinding.java
index 068f9af..8852a0d 100644
--- a/core/src/main/java/org/apache/calcite/sql/SqlCallBinding.java
+++ b/core/src/main/java/org/apache/calcite/sql/SqlCallBinding.java
@@ -16,9 +16,7 @@
  */
 package org.apache.calcite.sql;
 
-import org.apache.calcite.adapter.enumerable.EnumUtils;
 import org.apache.calcite.rel.type.RelDataType;
-import org.apache.calcite.rel.type.RelDataTypeFactoryImpl;
 import org.apache.calcite.runtime.CalciteException;
 import org.apache.calcite.runtime.Resources;
 import org.apache.calcite.sql.fun.SqlLiteralChainOperator;
@@ -272,22 +270,6 @@ public class SqlCallBinding extends SqlOperatorBinding {
     return valueAs(node, clazz);
   }
 
-  @Override public @Nullable Object getOperandLiteralValue(int ordinal, RelDataType type) {
-    if (!(type instanceof RelDataTypeFactoryImpl.JavaType)) {
-      return null;
-    }
-    final Class<?> clazz = ((RelDataTypeFactoryImpl.JavaType) type).getJavaClass();
-    final Object o = getOperandLiteralValue(ordinal, Object.class);
-    if (o == null) {
-      return null;
-    }
-    if (clazz.isInstance(o)) {
-      return clazz.cast(o);
-    }
-    final Object o2 = o instanceof NlsString ? ((NlsString) o).getValue() : o;
-    return EnumUtils.evaluate(o2, clazz);
-  }
-
   private static <T extends Object> @Nullable T valueAs(SqlNode node, Class<T> clazz) {
     final SqlLiteral literal;
     switch (node.getKind()) {
diff --git a/core/src/main/java/org/apache/calcite/sql/SqlOperatorBinding.java b/core/src/main/java/org/apache/calcite/sql/SqlOperatorBinding.java
index 1693e89..9711385 100644
--- a/core/src/main/java/org/apache/calcite/sql/SqlOperatorBinding.java
+++ b/core/src/main/java/org/apache/calcite/sql/SqlOperatorBinding.java
@@ -16,12 +16,15 @@
  */
 package org.apache.calcite.sql;
 
+import org.apache.calcite.adapter.enumerable.EnumUtils;
 import org.apache.calcite.rel.type.RelDataType;
 import org.apache.calcite.rel.type.RelDataTypeFactory;
+import org.apache.calcite.rel.type.RelDataTypeFactoryImpl;
 import org.apache.calcite.runtime.CalciteException;
 import org.apache.calcite.runtime.Resources;
 import org.apache.calcite.sql.validate.SqlMonotonicity;
 import org.apache.calcite.sql.validate.SqlValidatorException;
+import org.apache.calcite.util.NlsString;
 
 import org.checkerframework.checker.nullness.qual.Nullable;
 
@@ -146,9 +149,22 @@ public abstract class SqlOperatorBinding {
    * @return value of operand
    */
   public @Nullable Object getOperandLiteralValue(int ordinal, RelDataType type) {
-    throw new UnsupportedOperationException();
+    if (!(type instanceof RelDataTypeFactoryImpl.JavaType)) {
+      return null;
+    }
+    final Class<?> clazz = ((RelDataTypeFactoryImpl.JavaType) type).getJavaClass();
+    final Object o = getOperandLiteralValue(ordinal, Object.class);
+    if (o == null) {
+      return null;
+    }
+    if (clazz.isInstance(o)) {
+      return clazz.cast(o);
+    }
+    final Object o2 = o instanceof NlsString ? ((NlsString) o).getValue() : o;
+    return EnumUtils.evaluate(o2, clazz);
   }
 
+
   @Deprecated // to be removed before 2.0
   public @Nullable Comparable getOperandLiteralValue(int ordinal) {
     return getOperandLiteralValue(ordinal, Comparable.class);
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 f24659b..1b95cc4 100644
--- a/core/src/test/java/org/apache/calcite/test/InterpreterTest.java
+++ b/core/src/test/java/org/apache/calcite/test/InterpreterTest.java
@@ -31,6 +31,9 @@ import org.apache.calcite.rel.rules.CoreRules;
 import org.apache.calcite.rel.type.RelDataType;
 import org.apache.calcite.rel.type.RelDataTypeField;
 import org.apache.calcite.schema.SchemaPlus;
+import org.apache.calcite.schema.TableFunction;
+import org.apache.calcite.schema.impl.AbstractSchema;
+import org.apache.calcite.schema.impl.TableFunctionImpl;
 import org.apache.calcite.sql.SqlNode;
 import org.apache.calcite.sql.parser.SqlParseException;
 import org.apache.calcite.sql.parser.SqlParser;
@@ -39,6 +42,7 @@ import org.apache.calcite.tools.Frameworks;
 import org.apache.calcite.tools.Planner;
 import org.apache.calcite.tools.RelConversionException;
 import org.apache.calcite.tools.ValidationException;
+import org.apache.calcite.util.Smalls;
 import org.apache.calcite.util.Util;
 
 import org.checkerframework.checker.nullness.qual.Nullable;
@@ -539,4 +543,51 @@ class InterpreterTest {
             "[7839, 1981-11-17]", "[7844, 1981-09-08]", "[7876, 1987-05-23]",
             "[7900, 1981-12-03]", "[7902, 1981-12-03]", "[7934, 1982-01-23]");
   }
+
+  /** Tests a table function. */
+  @Test void testInterpretTableFunction() {
+    SchemaPlus schema = rootSchema.add("s", new AbstractSchema());
+    final TableFunction table1 = TableFunctionImpl.create(Smalls.MAZE_METHOD);
+    schema.add("Maze", table1);
+    final String sql = "select *\n"
+        + "from table(\"s\".\"Maze\"(5, 3, 1))";
+    String[] rows = {"[abcde]", "[xyz]", "[generate(w=5, h=3, s=1)]"};
+    sql(sql).returnsRows(rows);
+  }
+
+  /** Tests a table function that takes zero arguments.
+   *
+   * <p>Note that we use {@link Smalls#FIBONACCI_LIMIT_100_TABLE_METHOD}; if we
+   * used {@link Smalls#FIBONACCI_TABLE_METHOD}, even with {@code LIMIT 6},
+   * we would run out of memory, due to
+   * <a href="https://issues.apache.org/jira/browse/CALCITE-4478">[CALCITE-4478]
+   * In interpreter, support infinite relations</a>. */
+  @Test void testInterpretNilaryTableFunction() {
+    SchemaPlus schema = rootSchema.add("s", new AbstractSchema());
+    final TableFunction table1 =
+        TableFunctionImpl.create(Smalls.FIBONACCI_LIMIT_100_TABLE_METHOD);
+    schema.add("fibonacciLimit100", table1);
+    final String sql = "select *\n"
+        + "from table(\"s\".\"fibonacciLimit100\"())\n"
+        + "limit 6";
+    String[] rows = {"[1]", "[1]", "[2]", "[3]", "[5]", "[8]"};
+    sql(sql).returnsRows(rows);
+  }
+
+  /** Tests a table function whose row type is determined by parsing a JSON
+   * argument. */
+  @Test void testInterpretTableFunctionWithDynamicType() {
+    SchemaPlus schema = rootSchema.add("s", new AbstractSchema());
+    final TableFunction table1 =
+        TableFunctionImpl.create(Smalls.DYNAMIC_ROW_TYPE_TABLE_METHOD);
+    schema.add("dynamicRowTypeTable", table1);
+    final String sql = "select *\n"
+        + "from table(\"s\".\"dynamicRowTypeTable\"('"
+        + "{\"nullable\":false,\"fields\":["
+        + "  {\"name\":\"i\",\"type\":\"INTEGER\",\"nullable\":false},"
+        + "  {\"name\":\"d\",\"type\":\"DATE\",\"nullable\":true}"
+        + "]}', 0))\n"
+        + "where \"i\" < 0 and \"d\" is not null";
+    sql(sql).returnsRows();
+  }
 }
diff --git a/core/src/test/java/org/apache/calcite/test/MockSqlOperatorTable.java b/core/src/test/java/org/apache/calcite/test/MockSqlOperatorTable.java
index cb42bff..81523de 100644
--- a/core/src/test/java/org/apache/calcite/test/MockSqlOperatorTable.java
+++ b/core/src/test/java/org/apache/calcite/test/MockSqlOperatorTable.java
@@ -92,6 +92,25 @@ public class MockSqlOperatorTable extends ChainedSqlOperatorTable {
     }
   }
 
+  /** "DYNTYPE" user-defined table function. */
+  public static class DynamicTypeFunction extends SqlFunction
+      implements SqlTableFunction {
+    public DynamicTypeFunction() {
+      super("RAMP",
+          SqlKind.OTHER_FUNCTION,
+          ReturnTypes.CURSOR,
+          null,
+          OperandTypes.NUMERIC,
+          SqlFunctionCategory.USER_DEFINED_TABLE_FUNCTION);
+    }
+
+    @Override public SqlReturnTypeInference getRowTypeInference() {
+      return opBinding -> opBinding.getTypeFactory().builder()
+          .add("I", SqlTypeName.INTEGER)
+          .build();
+    }
+  }
+
   /** Not valid as a table function, even though it returns CURSOR, because
    * it does not implement {@link SqlTableFunction}. */
   public static class NotATableFunction extends SqlFunction {
diff --git a/core/src/test/java/org/apache/calcite/test/RelBuilderTest.java b/core/src/test/java/org/apache/calcite/test/RelBuilderTest.java
index b083332..aee5b9d 100644
--- a/core/src/test/java/org/apache/calcite/test/RelBuilderTest.java
+++ b/core/src/test/java/org/apache/calcite/test/RelBuilderTest.java
@@ -49,14 +49,25 @@ import org.apache.calcite.rex.RexOver;
 import org.apache.calcite.rex.RexWindowBounds;
 import org.apache.calcite.runtime.CalciteException;
 import org.apache.calcite.schema.SchemaPlus;
+import org.apache.calcite.schema.TableFunction;
+import org.apache.calcite.schema.impl.TableFunctionImpl;
 import org.apache.calcite.schema.impl.ViewTable;
 import org.apache.calcite.schema.impl.ViewTableMacro;
+import org.apache.calcite.sql.SqlIdentifier;
+import org.apache.calcite.sql.SqlKind;
 import org.apache.calcite.sql.SqlMatchRecognize;
 import org.apache.calcite.sql.SqlOperator;
 import org.apache.calcite.sql.fun.SqlLibraryOperators;
 import org.apache.calcite.sql.fun.SqlStdOperatorTable;
 import org.apache.calcite.sql.parser.SqlParser;
+import org.apache.calcite.sql.parser.SqlParserPos;
+import org.apache.calcite.sql.type.InferTypes;
+import org.apache.calcite.sql.type.OperandTypes;
+import org.apache.calcite.sql.type.ReturnTypes;
+import org.apache.calcite.sql.type.SqlOperandMetadata;
+import org.apache.calcite.sql.type.SqlTypeFamily;
 import org.apache.calcite.sql.type.SqlTypeName;
+import org.apache.calcite.sql.validate.SqlUserDefinedTableFunction;
 import org.apache.calcite.tools.Frameworks;
 import org.apache.calcite.tools.Programs;
 import org.apache.calcite.tools.RelBuilder;
@@ -65,6 +76,7 @@ import org.apache.calcite.tools.RelRunners;
 import org.apache.calcite.util.Holder;
 import org.apache.calcite.util.ImmutableBitSet;
 import org.apache.calcite.util.Pair;
+import org.apache.calcite.util.Smalls;
 import org.apache.calcite.util.TimestampString;
 import org.apache.calcite.util.Util;
 import org.apache.calcite.util.mapping.Mappings;
@@ -78,6 +90,7 @@ import org.checkerframework.checker.nullness.qual.Nullable;
 import org.hamcrest.Matcher;
 import org.junit.jupiter.api.Test;
 
+import java.lang.reflect.Method;
 import java.sql.Connection;
 import java.sql.DriverManager;
 import java.sql.PreparedStatement;
@@ -94,6 +107,7 @@ import java.util.concurrent.atomic.AtomicInteger;
 import java.util.function.BiFunction;
 import java.util.function.Function;
 import java.util.function.UnaryOperator;
+import java.util.stream.Collectors;
 
 import static org.apache.calcite.test.Matchers.hasHints;
 import static org.apache.calcite.test.Matchers.hasTree;
@@ -384,6 +398,52 @@ public class RelBuilderTest {
     }
   }
 
+  /** Tests scanning a table function whose row type is determined by parsing a
+   * JSON argument. The arguments must therefore be available at prepare
+   * time. */
+  @Test void testTableFunctionScanDynamicType() {
+    // Equivalent SQL:
+    //   SELECT *
+    //   FROM TABLE("dynamicRowType"('{nullable:true,fields:[...]}', 3))
+    final RelBuilder builder = RelBuilder.create(config().build());
+    final Method m = Smalls.DYNAMIC_ROW_TYPE_TABLE_METHOD;
+    final TableFunction tableFunction =
+        TableFunctionImpl.create(m.getDeclaringClass(), m.getName());
+    final SqlOperator operator =
+        new SqlUserDefinedTableFunction(
+            new SqlIdentifier("dynamicRowType", SqlParserPos.ZERO),
+            SqlKind.OTHER_FUNCTION, ReturnTypes.CURSOR, InferTypes.ANY_NULLABLE,
+            Arg.metadata(
+                Arg.of("count", f -> f.createSqlType(SqlTypeName.INTEGER),
+                    SqlTypeFamily.INTEGER, false),
+                Arg.of("typeJson", f -> f.createSqlType(SqlTypeName.VARCHAR),
+                    SqlTypeFamily.STRING, false)),
+            tableFunction);
+
+    final String jsonRowType = "{\"nullable\":false,\"fields\":["
+        + "  {\"name\":\"i\",\"type\":\"INTEGER\",\"nullable\":false},"
+        + "  {\"name\":\"d\",\"type\":\"DATE\",\"nullable\":true}"
+        + "]}";
+    final int rowCount = 3;
+    RelNode root = builder.functionScan(operator, 0,
+        builder.literal(jsonRowType), builder.literal(rowCount))
+        .build();
+    final String expected = "LogicalTableFunctionScan("
+        + "invocation=[dynamicRowType('{\"nullable\":false,\"fields\":["
+        + "  {\"name\":\"i\",\"type\":\"INTEGER\",\"nullable\":false},"
+        + "  {\"name\":\"d\",\"type\":\"DATE\",\"nullable\":true}]}', 3)], "
+        + "rowType=[RecordType(INTEGER i, DATE d)])\n";
+    assertThat(root, hasTree(expected));
+
+    // Make sure that the builder's stack is empty.
+    try {
+      RelNode node = builder.build();
+      fail("expected error, got " + node);
+    } catch (NoSuchElementException e) {
+      assertNull(e.getMessage());
+    }
+  }
+
   @Test void testJoinTemporalTable() {
     // Equivalent SQL:
     //   SELECT *
@@ -4118,4 +4178,43 @@ public class RelBuilderTest {
             "empid=110; name=Theodore",
             "empid=150; name=Sebastian");
   }
+
+  /** Operand to a user-defined function. */
+  private interface Arg {
+    String name();
+    RelDataType type(RelDataTypeFactory typeFactory);
+    SqlTypeFamily family();
+    boolean optional();
+
+    static SqlOperandMetadata metadata(Arg... args) {
+      return OperandTypes.operandMetadata(
+          Arrays.stream(args).map(Arg::family).collect(Collectors.toList()),
+          typeFactory ->
+              Arrays.stream(args).map(arg -> arg.type(typeFactory))
+                  .collect(Collectors.toList()),
+          i -> args[i].name(), i -> args[i].optional());
+    }
+
+    static Arg of(String name,
+        Function<RelDataTypeFactory, RelDataType> protoType,
+        SqlTypeFamily family, boolean optional) {
+      return new Arg() {
+        @Override public String name() {
+          return name;
+        }
+
+        @Override public RelDataType type(RelDataTypeFactory typeFactory) {
+          return protoType.apply(typeFactory);
+        }
+
+        @Override public SqlTypeFamily family() {
+          return family;
+        }
+
+        @Override public boolean optional() {
+          return optional;
+        }
+      };
+    }
+  }
 }
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 f0f0148..8a4ac9d 100644
--- a/core/src/test/java/org/apache/calcite/test/TableFunctionTest.java
+++ b/core/src/test/java/org/apache/calcite/test/TableFunctionTest.java
@@ -57,7 +57,7 @@ class TableFunctionTest {
     final String c = Smalls.class.getName();
     final String m = Smalls.MULTIPLICATION_TABLE_METHOD.getName();
     final String m2 = Smalls.FIBONACCI_TABLE_METHOD.getName();
-    final String m3 = Smalls.FIBONACCI2_TABLE_METHOD.getName();
+    final String m3 = Smalls.FIBONACCI_LIMIT_TABLE_METHOD.getName();
     return CalciteAssert.model("{\n"
         + "  version: '1.0',\n"
         + "   schemas: [\n"
diff --git a/core/src/test/java/org/apache/calcite/util/Smalls.java b/core/src/test/java/org/apache/calcite/util/Smalls.java
index 03527b7..6dab428 100644
--- a/core/src/test/java/org/apache/calcite/util/Smalls.java
+++ b/core/src/test/java/org/apache/calcite/util/Smalls.java
@@ -30,6 +30,7 @@ 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.Types;
+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;
@@ -92,8 +93,15 @@ public class Smalls {
         int.class, Integer.class);
   public static final Method FIBONACCI_TABLE_METHOD =
       Types.lookupMethod(Smalls.class, "fibonacciTable");
-  public static final Method FIBONACCI2_TABLE_METHOD =
+  public static final Method FIBONACCI_LIMIT_100_TABLE_METHOD =
+      Types.lookupMethod(Smalls.class, "fibonacciTableWithLimit100");
+  public static final Method FIBONACCI_LIMIT_TABLE_METHOD =
       Types.lookupMethod(Smalls.class, "fibonacciTableWithLimit", long.class);
+  public static final Method FIBONACCI_INSTANCE_TABLE_METHOD =
+      Types.lookupMethod(Smalls.FibonacciTableFunction.class, "eval");
+  public static final Method DYNAMIC_ROW_TYPE_TABLE_METHOD =
+      Types.lookupMethod(Smalls.class, "dynamicRowTypeTable", String.class,
+          int.class);
   public static final Method VIEW_METHOD =
       Types.lookupMethod(Smalls.class, "view", String.class);
   public static final Method STR_METHOD =
@@ -234,11 +242,20 @@ public class Smalls {
   }
 
   /** A function that generates the Fibonacci sequence.
-   * Interesting because it has one column and no arguments. */
+   *
+   * <p>Interesting because it has one column and no arguments,
+   * and because it is infinite. */
   public static ScannableTable fibonacciTable() {
     return fibonacciTableWithLimit(-1L);
   }
 
+  /** A function that generates the first 100 terms of the Fibonacci sequence.
+   *
+   * <p>Interesting because it has one column and no arguments. */
+  public static ScannableTable fibonacciTableWithLimit100() {
+    return fibonacciTableWithLimit(100L);
+  }
+
   /** A function that generates the Fibonacci sequence.
    * Interesting because it has one column and no arguments. */
   public static ScannableTable fibonacciTableWithLimit(final long limit) {
@@ -299,6 +316,33 @@ public class Smalls {
     };
   }
 
+  public static ScannableTable dynamicRowTypeTable(String jsonRowType,
+      int rowCount) {
+    return new DynamicRowTypeTable(jsonRowType, rowCount);
+  }
+
+  /** A table whose row type is determined by parsing a JSON argument. */
+  private static class DynamicRowTypeTable extends AbstractTable
+      implements ScannableTable {
+    private final String jsonRowType;
+
+    DynamicRowTypeTable(String jsonRowType, int count) {
+      this.jsonRowType = jsonRowType;
+    }
+
+    @Override public RelDataType getRowType(RelDataTypeFactory typeFactory) {
+      try {
+        return RelJsonReader.readType(typeFactory, jsonRowType);
+      } catch (IOException e) {
+        throw Util.throwAsRuntime(e);
+      }
+    }
+
+    @Override public Enumerable<@Nullable Object[]> scan(DataContext root) {
+      return Linq4j.emptyEnumerable();
+    }
+  }
+
   /** Table function that adds a number to the first column of input cursor. */
   public static QueryableTable processCursor(final int offset,
       final Enumerable<Object[]> a) {
@@ -479,6 +523,22 @@ public class Smalls {
     }
   }
 
+  /** Example of a UDF with non-default constructor.
+   *
+   * <p>Not used; we do not currently have a way to instantiate function
+   * objects other than via their default constructor. */
+  public static class FibonacciTableFunction {
+    private final int limit;
+
+    public FibonacciTableFunction(int limit) {
+      this.limit = limit;
+    }
+
+    public ScannableTable eval() {
+      return fibonacciTableWithLimit(limit);
+    }
+  }
+
   /** User-defined function with two arguments. */
   public static class MyIncrement {
     public float eval(int x, int y) {