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 2015/10/13 23:27:26 UTC

[2/2] incubator-calcite git commit: [CALCITE-916] Support table function that implements ScannableTable

[CALCITE-916] Support table function that implements ScannableTable

Add a new model, example/function, to contain examples of user-defined functions.

Add example table function "MAZE" that generates a maze.

When defining table functions in a model file, allow them to have a method name
other than "eval".


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

Branch: refs/heads/master
Commit: 5eb395c9ff5f60a409332b1a32536629d2d3f92e
Parents: 1007e54
Author: Julian Hyde <jh...@apache.org>
Authored: Sun Oct 11 08:44:26 2015 -0700
Committer: Julian Hyde <jh...@apache.org>
Committed: Tue Oct 13 10:17:59 2015 -0700

----------------------------------------------------------------------
 .../enumerable/EnumerableTableFunctionScan.java |  54 +++-
 .../org/apache/calcite/model/ModelHandler.java  |   7 +-
 .../rel/logical/LogicalTableFunctionScan.java   |   1 +
 .../calcite/schema/impl/TableFunctionImpl.java  |  59 ++--
 .../org/apache/calcite/util/BuiltInMethod.java  |   1 +
 .../java/org/apache/calcite/test/JdbcTest.java  |  63 +++-
 example/function/pom.xml                        | 101 ++++++
 .../org/apache/calcite/example/maze/Maze.java   | 314 +++++++++++++++++++
 .../apache/calcite/example/maze/MazeTable.java  |  88 ++++++
 .../calcite/example/maze/package-info.java      |  26 ++
 .../calcite/test/ExampleFunctionTest.java       |  74 +++++
 example/function/src/test/resources/model.json  |  33 ++
 example/pom.xml                                 |   1 +
 sqlline                                         |   2 +-
 14 files changed, 784 insertions(+), 40 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-calcite/blob/5eb395c9/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableTableFunctionScan.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableTableFunctionScan.java b/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableTableFunctionScan.java
index d8baf19..90892fa 100644
--- a/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableTableFunctionScan.java
+++ b/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableTableFunctionScan.java
@@ -26,8 +26,14 @@ import org.apache.calcite.rel.RelNode;
 import org.apache.calcite.rel.core.TableFunctionScan;
 import org.apache.calcite.rel.metadata.RelColumnMapping;
 import org.apache.calcite.rel.type.RelDataType;
+import org.apache.calcite.rex.RexCall;
 import org.apache.calcite.rex.RexNode;
+import org.apache.calcite.schema.QueryableTable;
+import org.apache.calcite.schema.impl.TableFunctionImpl;
+import org.apache.calcite.sql.validate.SqlUserDefinedTableFunction;
+import org.apache.calcite.util.BuiltInMethod;
 
+import java.lang.reflect.Method;
 import java.lang.reflect.Type;
 import java.util.List;
 import java.util.Set;
@@ -59,22 +65,52 @@ public class EnumerableTableFunctionScan extends TableFunctionScan
   public Result implement(EnumerableRelImplementor implementor, Prefer pref) {
     BlockBuilder bb = new BlockBuilder();
      // Non-array user-specified types are not supported yet
+    final JavaRowFormat format;
+    boolean array = false;
+    if (getElementType() == null) {
+      format = JavaRowFormat.ARRAY;
+    } else if (rowType.getFieldCount() == 1 && isQueryable()) {
+      format = JavaRowFormat.SCALAR;
+    } else if (getElementType() instanceof Class
+        && Object[].class.isAssignableFrom((Class) getElementType())) {
+      array = true;
+      format = JavaRowFormat.ARRAY;
+    } else {
+      format = JavaRowFormat.CUSTOM;
+    }
     final PhysType physType =
-        PhysTypeImpl.of(
-            implementor.getTypeFactory(),
-            getRowType(),
-            getElementType() == null /* e.g. not known */
-            || (getElementType() instanceof Class
-                && Object[].class.isAssignableFrom((Class) getElementType()))
-            ? JavaRowFormat.ARRAY
-            : JavaRowFormat.CUSTOM);
+        PhysTypeImpl.of(implementor.getTypeFactory(), getRowType(), format,
+            false);
     RexToLixTranslator t = RexToLixTranslator.forAggregation(
         (JavaTypeFactory) getCluster().getTypeFactory(), bb, null);
     t = t.setCorrelates(implementor.allCorrelateVariables);
-    final Expression translated = t.translate(getCall());
+    Expression translated = t.translate(getCall());
+    if (array && rowType.getFieldCount() == 1) {
+      translated =
+          Expressions.call(null, BuiltInMethod.SLICE0.method, translated);
+    }
     bb.add(Expressions.return_(null, translated));
     return implementor.result(physType, bb.toBlock());
   }
+
+  private boolean isQueryable() {
+    if (!(getCall() instanceof RexCall)) {
+      return false;
+    }
+    final RexCall call = (RexCall) getCall();
+    if (!(call.getOperator() instanceof SqlUserDefinedTableFunction)) {
+      return false;
+    }
+    final SqlUserDefinedTableFunction udtf =
+        (SqlUserDefinedTableFunction) call.getOperator();
+    if (!(udtf.getFunction() instanceof TableFunctionImpl)) {
+      return false;
+    }
+    final TableFunctionImpl tableFunction =
+        (TableFunctionImpl) udtf.getFunction();
+    final Method method = tableFunction.method;
+    return QueryableTable.class.isAssignableFrom(method.getReturnType());
+  }
 }
 
 // End EnumerableTableFunctionScan.java

http://git-wip-us.apache.org/repos/asf/incubator-calcite/blob/5eb395c9/core/src/main/java/org/apache/calcite/model/ModelHandler.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/calcite/model/ModelHandler.java b/core/src/main/java/org/apache/calcite/model/ModelHandler.java
index 3faf446..0b7afbe 100644
--- a/core/src/main/java/org/apache/calcite/model/ModelHandler.java
+++ b/core/src/main/java/org/apache/calcite/model/ModelHandler.java
@@ -96,13 +96,14 @@ public class ModelHandler {
       throw new RuntimeException("UDF class '"
           + className + "' not found");
     }
-    // Must look for TableMacro before ScalarFunction. Both have an "eval"
-    // method.
-    final TableFunction tableFunction = TableFunctionImpl.create(clazz);
+    final TableFunction tableFunction =
+        TableFunctionImpl.create(clazz, Util.first(methodName, "eval"));
     if (tableFunction != null) {
       schema.add(functionName, tableFunction);
       return;
     }
+    // Must look for TableMacro before ScalarFunction. Both have an "eval"
+    // method.
     final TableMacro macro = TableMacroImpl.create(clazz);
     if (macro != null) {
       schema.add(functionName, macro);

http://git-wip-us.apache.org/repos/asf/incubator-calcite/blob/5eb395c9/core/src/main/java/org/apache/calcite/rel/logical/LogicalTableFunctionScan.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/calcite/rel/logical/LogicalTableFunctionScan.java b/core/src/main/java/org/apache/calcite/rel/logical/LogicalTableFunctionScan.java
index c21720f..79102db 100644
--- a/core/src/main/java/org/apache/calcite/rel/logical/LogicalTableFunctionScan.java
+++ b/core/src/main/java/org/apache/calcite/rel/logical/LogicalTableFunctionScan.java
@@ -103,6 +103,7 @@ public class LogicalTableFunctionScan extends TableFunctionScan {
     assert traitSet.containsIfApplicable(Convention.NONE);
     return new LogicalTableFunctionScan(
         getCluster(),
+        traitSet,
         inputs,
         rexCall,
         elementType,

http://git-wip-us.apache.org/repos/asf/incubator-calcite/blob/5eb395c9/core/src/main/java/org/apache/calcite/schema/impl/TableFunctionImpl.java
----------------------------------------------------------------------
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 b23f3a4..1ef3dfd 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
@@ -29,7 +29,9 @@ import org.apache.calcite.rel.type.RelDataTypeFactory;
 import org.apache.calcite.rex.RexCall;
 import org.apache.calcite.schema.ImplementableFunction;
 import org.apache.calcite.schema.QueryableTable;
+import org.apache.calcite.schema.ScannableTable;
 import org.apache.calcite.schema.SchemaPlus;
+import org.apache.calcite.schema.Table;
 import org.apache.calcite.schema.TableFunction;
 import org.apache.calcite.util.BuiltInMethod;
 
@@ -59,7 +61,13 @@ public class TableFunctionImpl extends ReflectiveFunctionBase implements
   /** Creates a {@link TableFunctionImpl} from a class, looking for an "eval"
    * method. Returns null if there is no such method. */
   public static TableFunction create(Class<?> clazz) {
-    final Method method = findMethod(clazz, "eval");
+    return create(clazz, "eval");
+  }
+
+  /** Creates a {@link TableFunctionImpl} from a class, looking for a method
+   * with a given name. Returns null if there is no such method. */
+  public static TableFunction create(Class<?> clazz, String methodName) {
+    final Method method = findMethod(clazz, methodName);
     if (method == null) {
       return null;
     }
@@ -75,7 +83,8 @@ public class TableFunctionImpl extends ReflectiveFunctionBase implements
       }
     }
     final Class<?> returnType = method.getReturnType();
-    if (!QueryableTable.class.isAssignableFrom(returnType)) {
+    if (!QueryableTable.class.isAssignableFrom(returnType)
+        && !ScannableTable.class.isAssignableFrom(returnType)) {
       return null;
     }
     CallImplementor implementor = createImplementor(method);
@@ -88,7 +97,15 @@ public class TableFunctionImpl extends ReflectiveFunctionBase implements
   }
 
   public Type getElementType(List<Object> arguments) {
-    return apply(arguments).getElementType();
+    final Table table = apply(arguments);
+    if (table instanceof QueryableTable) {
+      QueryableTable queryableTable = (QueryableTable) table;
+      return queryableTable.getElementType();
+    } else if (table instanceof ScannableTable) {
+      return Object[].class;
+    }
+    throw new AssertionError("Invalid table class: " + table + " "
+        + table.getClass());
   }
 
   public CallImplementor getImplementor() {
@@ -102,22 +119,27 @@ public class TableFunctionImpl extends ReflectiveFunctionBase implements
               RexCall call, List<Expression> translatedOperands) {
             Expression expr = super.implement(translator, call,
                 translatedOperands);
-            Expression queryable = Expressions.call(
-              Expressions.convert_(expr, QueryableTable.class),
-              BuiltInMethod.QUERYABLE_TABLE_AS_QUERYABLE.method,
-              Expressions.call(DataContext.ROOT,
-                BuiltInMethod.DATA_CONTEXT_GET_QUERY_PROVIDER.method),
-              Expressions.constant(null, SchemaPlus.class),
-              Expressions.constant(call.getOperator().getName(),
-                String.class));
-            expr = Expressions.call(queryable,
-                BuiltInMethod.QUERYABLE_AS_ENUMERABLE.method);
+            final Class<?> returnType = method.getReturnType();
+            if (QueryableTable.class.isAssignableFrom(returnType)) {
+              Expression queryable = Expressions.call(
+                  Expressions.convert_(expr, QueryableTable.class),
+                  BuiltInMethod.QUERYABLE_TABLE_AS_QUERYABLE.method,
+                  Expressions.call(DataContext.ROOT,
+                      BuiltInMethod.DATA_CONTEXT_GET_QUERY_PROVIDER.method),
+                  Expressions.constant(null, SchemaPlus.class),
+                  Expressions.constant(call.getOperator().getName(), String.class));
+              expr = Expressions.call(queryable,
+                  BuiltInMethod.QUERYABLE_AS_ENUMERABLE.method);
+            } else {
+              expr = Expressions.call(expr,
+                  BuiltInMethod.SCANNABLE_TABLE_SCAN.method, DataContext.ROOT);
+            }
             return expr;
           }
         }, NullPolicy.ANY, false);
   }
 
-  private QueryableTable apply(List<Object> arguments) {
+  private Table apply(List<Object> arguments) {
     try {
       Object o = null;
       if (!Modifier.isStatic(method.getModifiers())) {
@@ -125,18 +147,15 @@ public class TableFunctionImpl extends ReflectiveFunctionBase implements
       }
       //noinspection unchecked
       final Object table = method.invoke(o, arguments.toArray());
-      return (QueryableTable) table;
+      return (Table) table;
     } catch (IllegalArgumentException e) {
       throw RESOURCE.illegalArgumentForTableFunctionCall(
           method.toString(),
           Arrays.toString(method.getParameterTypes()),
           arguments.toString()
       ).ex(e);
-    } catch (IllegalAccessException e) {
-      throw new RuntimeException(e);
-    } catch (InvocationTargetException e) {
-      throw new RuntimeException(e);
-    } catch (InstantiationException e) {
+    } catch (IllegalAccessException | InvocationTargetException
+        | InstantiationException e) {
       throw new RuntimeException(e);
     }
   }

http://git-wip-us.apache.org/repos/asf/incubator-calcite/blob/5eb395c9/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 ed2e2ea..287b677 100644
--- a/core/src/main/java/org/apache/calcite/util/BuiltInMethod.java
+++ b/core/src/main/java/org/apache/calcite/util/BuiltInMethod.java
@@ -255,6 +255,7 @@ public enum BuiltInMethod {
   NOT(SqlFunctions.class, "not", Boolean.class),
   MODIFIABLE_TABLE_GET_MODIFIABLE_COLLECTION(ModifiableTable.class,
       "getModifiableCollection"),
+  SCANNABLE_TABLE_SCAN(ScannableTable.class, "scan", DataContext.class),
   STRING_TO_BOOLEAN(SqlFunctions.class, "toBoolean", String.class),
   STRING_TO_DATE(DateTimeUtils.class, "dateStringToUnixDate", String.class),
   STRING_TO_TIME(DateTimeUtils.class, "timeStringToUnixDate", String.class),

http://git-wip-us.apache.org/repos/asf/incubator-calcite/blob/5eb395c9/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 a95fadf..e62c4d0 100644
--- a/core/src/test/java/org/apache/calcite/test/JdbcTest.java
+++ b/core/src/test/java/org/apache/calcite/test/JdbcTest.java
@@ -16,6 +16,7 @@
  */
 package org.apache.calcite.test;
 
+import org.apache.calcite.DataContext;
 import org.apache.calcite.adapter.clone.CloneSchema;
 import org.apache.calcite.adapter.generate.RangeTable;
 import org.apache.calcite.adapter.java.AbstractQueryableTable;
@@ -62,6 +63,7 @@ import org.apache.calcite.runtime.SqlFunctions;
 import org.apache.calcite.schema.ModifiableTable;
 import org.apache.calcite.schema.ModifiableView;
 import org.apache.calcite.schema.QueryableTable;
+import org.apache.calcite.schema.ScannableTable;
 import org.apache.calcite.schema.Schema;
 import org.apache.calcite.schema.SchemaFactory;
 import org.apache.calcite.schema.SchemaPlus;
@@ -171,6 +173,10 @@ public class JdbcTest {
   public static final Method GENERATE_STRINGS_METHOD =
       Types.lookupMethod(JdbcTest.class, "generateStrings", Integer.class);
 
+  public static final Method MAZE_METHOD =
+      Types.lookupMethod(MazeTable.class, "generate", int.class, int.class,
+          int.class);
+
   public static final Method MULTIPLICATION_TABLE_METHOD =
       Types.lookupMethod(JdbcTest.class, "multiplicationTable", int.class,
         int.class, Integer.class);
@@ -454,6 +460,27 @@ public class JdbcTest {
   }
 
   /**
+   * Tests a table function that implements {@link ScannableTable} and returns
+   * a single column.
+   */
+  @Test public void testScannableTableFunction()
+      throws SQLException, ClassNotFoundException {
+    Connection connection = DriverManager.getConnection("jdbc:calcite:");
+    CalciteConnection calciteConnection =
+        connection.unwrap(CalciteConnection.class);
+    SchemaPlus rootSchema = calciteConnection.getRootSchema();
+    SchemaPlus schema = rootSchema.add("s", new AbstractSchema());
+    final TableFunction table = TableFunctionImpl.create(MAZE_METHOD);
+    schema.add("Maze", table);
+    final String sql = "select *\n"
+        + "from table(\"s\".\"Maze\"(5, 3, 1))";
+    ResultSet resultSet = connection.createStatement().executeQuery(sql);
+    final String result = "S=abcde\n"
+        + "S=xyz\n";
+    assertThat(CalciteAssert.toString(resultSet), equalTo(result));
+  }
+
+  /**
    * Tests a table function that returns different row type based on
    * actual call arguments.
    */
@@ -4290,9 +4317,9 @@ public class JdbcTest {
     CalciteAssert.that()
         .with(CalciteAssert.Config.REGULAR)
         .query(
-            "select \"deptno\", \"employees\"[1] as e from \"hr\".\"depts\"\n")
-        .returnsUnordered("deptno=10; E={100, 10, Bill, 10000.0, 1000}",
-            "deptno=30; E=null",
+            "select \"deptno\", \"employees\"[1] as e from \"hr\".\"depts\"\n").returnsUnordered(
+        "deptno=10; E={100, 10, Bill, 10000.0, 1000}",
+        "deptno=30; E=null",
             "deptno=40; E={200, 20, Eric, 8000.0, 500}");
   }
 
@@ -7050,7 +7077,7 @@ public class JdbcTest {
   private static QueryableTable oneThreePlus(String s) {
     List<Integer> items;
     // Argument is null in case SQL contains function call with expression.
-    // Then the engine calls a function with null argumets to get getRowType.
+    // Then the engine calls a function with null arguments to get getRowType.
     if (s == null) {
       items = ImmutableList.of();
     } else {
@@ -7058,10 +7085,11 @@ public class JdbcTest {
       items = ImmutableList.of(1, 3, latest);
     }
     final Enumerable<Integer> enumerable = Linq4j.asEnumerable(items);
-    return new AbstractQueryableTable(Object[].class) {
-      public Queryable<Integer> asQueryable(
+    return new AbstractQueryableTable(Integer.class) {
+      public <E> Queryable<E> asQueryable(
           QueryProvider queryProvider, SchemaPlus schema, String tableName) {
-        return enumerable.asQueryable();
+        //noinspection unchecked
+        return (Queryable<E>) enumerable.asQueryable();
       }
 
       public RelDataType getRowType(RelDataTypeFactory typeFactory) {
@@ -7084,6 +7112,27 @@ public class JdbcTest {
       return oneThreePlus(s);
     }
   }
+
+  /** The real MazeTable may be found in example/function. This is a cut-down
+   * version to support a test. */
+  public static class MazeTable extends AbstractTable
+      implements ScannableTable {
+
+    public static ScannableTable generate(int width, int height, int seed) {
+      return new MazeTable();
+    }
+
+    public RelDataType getRowType(RelDataTypeFactory typeFactory) {
+      return typeFactory.builder()
+          .add("S", SqlTypeName.VARCHAR, 12)
+          .build();
+    }
+
+    public Enumerable<Object[]> scan(DataContext root) {
+      Object[][] rows = {{"abcde"}, {"xyz"}};
+      return Linq4j.asEnumerable(rows);
+    }
+  }
 }
 
 // End JdbcTest.java

http://git-wip-us.apache.org/repos/asf/incubator-calcite/blob/5eb395c9/example/function/pom.xml
----------------------------------------------------------------------
diff --git a/example/function/pom.xml b/example/function/pom.xml
new file mode 100644
index 0000000..93e6056
--- /dev/null
+++ b/example/function/pom.xml
@@ -0,0 +1,101 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+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.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+  <modelVersion>4.0.0</modelVersion>
+  <parent>
+    <groupId>org.apache.calcite</groupId>
+    <artifactId>calcite-example</artifactId>
+    <version>1.5.0-incubating-SNAPSHOT</version>
+  </parent>
+
+  <artifactId>calcite-example-function</artifactId>
+  <packaging>jar</packaging>
+  <version>1.5.0-incubating-SNAPSHOT</version>
+  <name>Calcite Example Function</name>
+  <description>Examples of user-defined Calcite functions</description>
+
+  <properties>
+    <top.dir>${project.basedir}/../..</top.dir>
+    <build.timestamp>${maven.build.timestamp}</build.timestamp>
+  </properties>
+
+  <dependencies>
+    <dependency>
+      <groupId>org.apache.calcite</groupId>
+      <artifactId>calcite-core</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.calcite</groupId>
+      <artifactId>calcite-linq4j</artifactId>
+    </dependency>
+
+    <dependency>
+      <groupId>junit</groupId>
+      <artifactId>junit</artifactId>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.hamcrest</groupId>
+      <artifactId>hamcrest-core</artifactId>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>sqlline</groupId>
+      <artifactId>sqlline</artifactId>
+      <scope>test</scope>
+    </dependency>
+  </dependencies>
+
+  <build>
+    <plugins>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-dependency-plugin</artifactId>
+        <version>2.10</version>
+        <!-- configurations do not cascade, so all of the definition from
+             ../pom.xml:build:plugin-management:plugins:plugin must be repeated in child poms -->
+        <executions>
+          <execution>
+            <id>analyze</id>
+            <goals>
+              <goal>analyze-only</goal>
+            </goals>
+            <configuration>
+              <failOnWarning>true</failOnWarning>
+              <!-- ignore "unused but declared" warnings -->
+              <ignoredUnusedDeclaredDependencies>
+                <ignoredUnusedDeclaredDependency>sqlline:sqlline</ignoredUnusedDeclaredDependency>
+              </ignoredUnusedDeclaredDependencies>
+            </configuration>
+          </execution>
+        </executions>
+      </plugin>
+      <plugin>
+        <artifactId>maven-remote-resources-plugin</artifactId>
+        <executions>
+          <execution>
+            <id>non-root-resources</id>
+            <goals>
+              <goal>process</goal>
+            </goals>
+          </execution>
+        </executions>
+      </plugin>
+    </plugins>
+  </build>
+</project>

http://git-wip-us.apache.org/repos/asf/incubator-calcite/blob/5eb395c9/example/function/src/main/java/org/apache/calcite/example/maze/Maze.java
----------------------------------------------------------------------
diff --git a/example/function/src/main/java/org/apache/calcite/example/maze/Maze.java b/example/function/src/main/java/org/apache/calcite/example/maze/Maze.java
new file mode 100644
index 0000000..3003c59
--- /dev/null
+++ b/example/function/src/main/java/org/apache/calcite/example/maze/Maze.java
@@ -0,0 +1,314 @@
+/*
+ * 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.example.maze;
+
+import org.apache.calcite.linq4j.Enumerator;
+
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Random;
+import java.util.Set;
+
+/** Maze generator. */
+class Maze {
+  private final int width;
+  final int height;
+  private final int[] regions;
+  private final boolean[] ups;
+  private final boolean[] lefts;
+
+  static final boolean DEBUG = false;
+  private final boolean horizontal = false;
+  private final boolean spiral = false;
+
+  public Maze(int width, int height) {
+    this.width = width;
+    this.height = height;
+    this.regions = new int[width * height];
+    for (int i = 0; i < regions.length; i++) {
+      regions[i] = i;
+    }
+    this.ups = new boolean[width * height + width];
+    this.lefts = new boolean[width * height + 1];
+  }
+
+  private int region(int cell) {
+    int region = regions[cell];
+    if (region == cell) {
+      return region;
+    }
+    return regions[cell] = region(region);
+  }
+
+  /** Prints the maze. Results are like this:
+   *
+   * <blockquote>
+   * +--+--+--+--+--+
+   * |        |     |
+   * +--+  +--+--+  +
+   * |     |  |     |
+   * +  +--+  +--+  +
+   * |              |
+   * +--+--+--+--+--+
+   * </blockquote>
+   *
+   * @param pw Print writer
+   * @param space Whether to put a space in each cell; if false, prints the
+   *              region number of the cell
+   */
+  public void print(PrintWriter pw, boolean space) {
+    pw.println();
+    final StringBuilder b = new StringBuilder();
+    final StringBuilder b2 = new StringBuilder();
+    for (int y = 0; y < height; y++) {
+      row(space, b, b2, y);
+      pw.println(b.toString());
+      pw.println(b2.toString());
+      b.setLength(0);
+      b2.setLength(0);
+    }
+    for (int x = 0; x < width; x++) {
+      pw.print("+--");
+    }
+    pw.println('+');
+    pw.flush();
+  }
+
+  /** Generates a list of lines representing the maze in text form. */
+  public Enumerator<String> enumerator() {
+    return new Enumerator<String>() {
+      int i = -1;
+      final StringBuilder b = new StringBuilder();
+      final StringBuilder b2 = new StringBuilder();
+
+      public String current() {
+        return i % 2 == 0 ? b.toString() : b2.toString();
+      }
+
+      public boolean moveNext() {
+        if (i >= height * 2) {
+          return false;
+        }
+        ++i;
+        if (i % 2 == 0) {
+          b.setLength(0);
+          b2.setLength(0);
+          row(true, b, b2, i / 2);
+        }
+        return true;
+      }
+
+      public void reset() {
+        i = -1;
+      }
+
+      public void close() {}
+    };
+  }
+
+  /** Returns a pair of strings representing a row of the maze. */
+  private void row(boolean space, StringBuilder b, StringBuilder b2, int y) {
+    final int c0 = y * width;
+    for (int x = 0; x < width; x++) {
+      b.append('+');
+      b.append(ups[c0 + x] ? "  " : "--");
+    }
+    b.append('+');
+    if (y == height) {
+      return;
+    }
+    for (int x = 0; x < width; x++) {
+      b2.append(lefts[c0 + x] ? ' ' : '|');
+      if (space) {
+        b2.append("  ");
+      } else {
+        String s = region(c0 + x) + "";
+        if (s.length() == 1) {
+          s = " " + s;
+        }
+        b2.append(s);
+      }
+    }
+    b2.append('|');
+  }
+
+  public Maze layout(Random random, PrintWriter pw) {
+    int[] candidates =
+        new int[width * height - width
+            + width * height - height];
+    int z = 0;
+    for (int y = 0, c = 0; y < height; y++) {
+      for (int x = 0; x < width; x++) {
+        if (x > 0) {
+          candidates[z++] = c;
+        }
+        ++c;
+        if (y > 0) {
+          candidates[z++] = c;
+        }
+        ++c;
+      }
+    }
+    assert z == candidates.length;
+    shuffle(random, candidates);
+
+    for (int candidate : candidates) {
+      final boolean up = (candidate & 1) != 0;
+      final int c = candidate >> 1;
+      if (up) {
+        int region = region(c - width);
+
+        // make sure we are not joining the same region, that is, making
+        // a cycle
+        if (region(c) != region) {
+          ups[c] = true;
+          regions[regions[c]] = region;
+          regions[c] = region;
+          if (DEBUG) {
+            pw.println("up " + c);
+          }
+        } else {
+          if (DEBUG) {
+            pw.println("cannot remove top wall at " + c);
+          }
+        }
+      } else {
+        int region = region(c - 1);
+
+        // make sure we are not joining the same region, that is, making
+        // a cycle
+        if (region(c) != region) {
+          lefts[c] = true;
+          regions[regions[c]] = region;
+          regions[c] = region;
+          if (DEBUG) {
+            pw.println("left " + c);
+          }
+        } else {
+          if (DEBUG) {
+            pw.println("cannot remove left wall at " + c);
+          }
+        }
+      }
+      if (DEBUG) {
+        print(pw, false);
+        print(pw, true);
+      }
+    }
+    return this;
+  }
+
+  private Set<Integer> solve(int x, int y) {
+    List<Integer> list = new ArrayList<>();
+    try {
+      solveRecurse(y * width + x, null, list);
+      return null;
+    } catch (SolvedException e) {
+      return new LinkedHashSet<>(e.list);
+    }
+  }
+
+  private void solveRecurse(int c, Direction direction, List<Integer> list) {
+    list.add(c);
+    if (c == regions.length - 1) {
+      throw new SolvedException(list);
+    }
+    // try to go up
+    if (direction != Direction.DOWN && ups[c]) {
+      solveRecurse(c - width, Direction.UP, list);
+    }
+    // try to go left
+    if (direction != Direction.RIGHT && lefts[c]) {
+      solveRecurse(c - 1, Direction.LEFT, list);
+    }
+    // try to go down
+    if (direction != Direction.UP
+        && c + width < regions.length && ups[c + width]) {
+      solveRecurse(c + width, Direction.DOWN, list);
+    }
+    // try to go right
+    if (direction != Direction.LEFT && c % width < width - 1 && lefts[c + 1]) {
+      solveRecurse(c + 1, Direction.RIGHT, list);
+    }
+    list.remove(list.size() - 1);
+  }
+
+  /** Direction. */
+  private enum Direction {
+    UP, LEFT, DOWN, RIGHT
+  }
+
+  /** Flow-control exception thrown when the maze is solved. */
+  private static class SolvedException extends RuntimeException {
+    private final List<Integer> list;
+
+    SolvedException(List<Integer> list) {
+      this.list = list;
+    }
+  }
+
+  /**
+   * Randomly permutes the members of an array. Based on the Fisher-Yates
+   * algorithm.
+   *
+   * @param random Random number generator
+   * @param ints Array of integers to shuffle
+   */
+  private void shuffle(Random random, int[] ints) {
+    for (int i = ints.length - 1; i > 0; i--) {
+      int j = random.nextInt(i + 1);
+      int t = ints[j];
+      ints[j] = ints[i];
+      ints[i] = t;
+    }
+
+    // move even walls (left) towards the start, so we end up with
+    // long horizontal corridors
+    if (horizontal) {
+      for (int i = 2; i < ints.length; i++) {
+        if (ints[i] % 2 == 0) {
+          int j = random.nextInt(i);
+          int t = ints[j];
+          ints[j] = ints[i];
+          ints[i] = t;
+        }
+      }
+    }
+
+    // move walls towards the edges towards the start
+    if (spiral) {
+      for (int z = 0; z < 5; z++) {
+        for (int i = 2; i < ints.length; i++) {
+          int x = ints[i] / 2 % width;
+          int y = ints[i] / 2 / width;
+          int xMin = Math.min(x, width - x);
+          int yMin = Math.min(y, height - y);
+          if (ints[i] % 2 == (xMin < yMin ? 1 : 0)) {
+            int j = random.nextInt(i);
+            int t = ints[j];
+            ints[j] = ints[i];
+            ints[i] = t;
+          }
+        }
+      }
+    }
+  }
+}
+
+// End Maze.java

http://git-wip-us.apache.org/repos/asf/incubator-calcite/blob/5eb395c9/example/function/src/main/java/org/apache/calcite/example/maze/MazeTable.java
----------------------------------------------------------------------
diff --git a/example/function/src/main/java/org/apache/calcite/example/maze/MazeTable.java b/example/function/src/main/java/org/apache/calcite/example/maze/MazeTable.java
new file mode 100644
index 0000000..b7f0150
--- /dev/null
+++ b/example/function/src/main/java/org/apache/calcite/example/maze/MazeTable.java
@@ -0,0 +1,88 @@
+/*
+ * 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.example.maze;
+
+import org.apache.calcite.DataContext;
+import org.apache.calcite.linq4j.AbstractEnumerable;
+import org.apache.calcite.linq4j.Enumerable;
+import org.apache.calcite.linq4j.Enumerator;
+import org.apache.calcite.linq4j.Linq4j;
+import org.apache.calcite.linq4j.function.Function1;
+import org.apache.calcite.rel.type.RelDataType;
+import org.apache.calcite.rel.type.RelDataTypeFactory;
+import org.apache.calcite.schema.ScannableTable;
+import org.apache.calcite.schema.impl.AbstractTable;
+import org.apache.calcite.sql.type.SqlTypeName;
+
+import java.io.PrintWriter;
+import java.util.Random;
+
+/**
+ * User-defined table function that generates a Maze and prints it in text
+ * form.
+ */
+public class MazeTable extends AbstractTable implements ScannableTable {
+  final int width;
+  final int height;
+  final int seed;
+
+  private MazeTable(int width, int height, int seed) {
+    this.width = width;
+    this.height = height;
+    this.seed = seed;
+  }
+
+  /** Called by reflection based on the definition of the user-defined
+   * function in the schema.
+   *
+   * @param width Width of maze
+   * @param height Height of maze
+   * @param seed Random number seed, or -1 to create an unseeded random
+   * @return Table that prints the maze in text form
+   */
+  public static ScannableTable generate(int width, int height, int seed) {
+    return new MazeTable(width, height, seed);
+  }
+
+  public RelDataType getRowType(RelDataTypeFactory typeFactory) {
+    return typeFactory.builder()
+        .add("S", SqlTypeName.VARCHAR, width * 3 + 1)
+        .build();
+  }
+
+  public Enumerable<Object[]> scan(DataContext root) {
+    final Random random = seed >= 0 ? new Random(seed) : new Random();
+    final Maze maze = new Maze(width, height);
+    final PrintWriter pw = new PrintWriter(System.out);
+    maze.layout(random, pw);
+    if (Maze.DEBUG) {
+      maze.print(pw, true);
+    }
+    return new AbstractEnumerable<Object[]>() {
+      public Enumerator<Object[]> enumerator() {
+        return Linq4j.transform(maze.enumerator(),
+            new Function1<String, Object[]>() {
+              public Object[] apply(String s) {
+                return new Object[] {s};
+              }
+            });
+      }
+    };
+  }
+}
+
+// End MazeTable.java

http://git-wip-us.apache.org/repos/asf/incubator-calcite/blob/5eb395c9/example/function/src/main/java/org/apache/calcite/example/maze/package-info.java
----------------------------------------------------------------------
diff --git a/example/function/src/main/java/org/apache/calcite/example/maze/package-info.java b/example/function/src/main/java/org/apache/calcite/example/maze/package-info.java
new file mode 100644
index 0000000..9c2072a
--- /dev/null
+++ b/example/function/src/main/java/org/apache/calcite/example/maze/package-info.java
@@ -0,0 +1,26 @@
+/*
+ * 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.
+ */
+
+/**
+ * User-defined table function that generates a maze.
+ */
+@PackageMarker
+package org.apache.calcite.example.maze;
+
+import org.apache.calcite.avatica.util.PackageMarker;
+
+// End package-info.java

http://git-wip-us.apache.org/repos/asf/incubator-calcite/blob/5eb395c9/example/function/src/test/java/org/apache/calcite/test/ExampleFunctionTest.java
----------------------------------------------------------------------
diff --git a/example/function/src/test/java/org/apache/calcite/test/ExampleFunctionTest.java b/example/function/src/test/java/org/apache/calcite/test/ExampleFunctionTest.java
new file mode 100644
index 0000000..80031a5
--- /dev/null
+++ b/example/function/src/test/java/org/apache/calcite/test/ExampleFunctionTest.java
@@ -0,0 +1,74 @@
+/*
+ * 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.test;
+
+import org.apache.calcite.example.maze.MazeTable;
+import org.apache.calcite.jdbc.CalciteConnection;
+import org.apache.calcite.linq4j.tree.Types;
+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.junit.Test;
+
+import java.lang.reflect.Method;
+import java.sql.Connection;
+import java.sql.DriverManager;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.junit.Assert.assertThat;
+
+/**
+ * Unit tests for example user-defined functions.
+ */
+public class ExampleFunctionTest {
+  public static final Method MAZE_METHOD =
+      Types.lookupMethod(MazeTable.class, "generate", int.class, int.class,
+          int.class);
+
+  /** Unit test for {@link MazeTable}. */
+  @Test public void testMazeTableFunction()
+      throws SQLException, ClassNotFoundException {
+    Connection connection = DriverManager.getConnection("jdbc:calcite:");
+    CalciteConnection calciteConnection =
+        connection.unwrap(CalciteConnection.class);
+    SchemaPlus rootSchema = calciteConnection.getRootSchema();
+    SchemaPlus schema = rootSchema.add("s", new AbstractSchema());
+    final TableFunction table = TableFunctionImpl.create(MAZE_METHOD);
+    schema.add("Maze", table);
+    ResultSet resultSet = connection.createStatement().executeQuery("select *\n"
+        + "from table(\"s\".\"Maze\"(5, 3, 1)) as t(s)");
+    final StringBuilder b = new StringBuilder();
+    while (resultSet.next()) {
+      b.append(resultSet.getString(1)).append("\n");
+    }
+    final String maze = ""
+        + "+--+--+--+--+--+\n"
+        + "|        |     |\n"
+        + "+--+  +--+--+  +\n"
+        + "|     |  |     |\n"
+        + "+  +--+  +--+  +\n"
+        + "|              |\n"
+        + "+--+--+--+--+--+\n";
+    assertThat(b.toString(), is(maze));
+  }
+}
+
+// End ExampleFunctionTest.java

http://git-wip-us.apache.org/repos/asf/incubator-calcite/blob/5eb395c9/example/function/src/test/resources/model.json
----------------------------------------------------------------------
diff --git a/example/function/src/test/resources/model.json b/example/function/src/test/resources/model.json
new file mode 100644
index 0000000..95d047c
--- /dev/null
+++ b/example/function/src/test/resources/model.json
@@ -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.
+ *
+ * A JSON model of a Calcite that contains various user-defined functions.
+ */
+{
+  "version": "1.0",
+  "defaultSchema": "MAZE",
+  "schemas": [
+    {
+      "name": "MAZE",
+      "type": "map",
+      "functions": [ {
+        "name": "MAZE",
+        "className": "org.apache.calcite.example.maze.MazeTable",
+        "methodName": "generate"
+      } ]
+    }
+  ]
+}

http://git-wip-us.apache.org/repos/asf/incubator-calcite/blob/5eb395c9/example/pom.xml
----------------------------------------------------------------------
diff --git a/example/pom.xml b/example/pom.xml
index b8748e4..bd2c067 100644
--- a/example/pom.xml
+++ b/example/pom.xml
@@ -37,5 +37,6 @@ limitations under the License.
 
   <modules>
     <module>csv</module>
+    <module>function</module>
   </modules>
 </project>

http://git-wip-us.apache.org/repos/asf/incubator-calcite/blob/5eb395c9/sqlline
----------------------------------------------------------------------
diff --git a/sqlline b/sqlline
index 4b13a0c..9f4156e 100755
--- a/sqlline
+++ b/sqlline
@@ -37,7 +37,7 @@ if [ ! -f target/fullclasspath.txt ]; then
 fi
 
 CP=
-for module in core avatica mongodb spark splunk; do
+for module in core avatica mongodb spark splunk example/csv example/function; do
   CP=${CP}${module}/target/classes:
   CP=${CP}${module}/target/test-classes:
 done