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