http://git-wip-us.apache.org/repos/asf/calcite/blob/d012b245/core/src/main/java/org/apache/calcite/sql/validate/SqlValidatorImpl.java ---------------------------------------------------------------------- diff --git a/core/src/main/java/org/apache/calcite/sql/validate/SqlValidatorImpl.java b/core/src/main/java/org/apache/calcite/sql/validate/SqlValidatorImpl.java index d234b6d..f20273e 100644 --- a/core/src/main/java/org/apache/calcite/sql/validate/SqlValidatorImpl.java +++ b/core/src/main/java/org/apache/calcite/sql/validate/SqlValidatorImpl.java @@ -1480,7 +1480,8 @@ public class SqlValidatorImpl implements SqlValidatorWithHints { if (call.operandCount() > 0) { // This is not a default constructor invocation, and // no user-defined constructor could be found - throw handleUnresolvedFunction(call, unresolvedConstructor, argTypes); + throw handleUnresolvedFunction(call, unresolvedConstructor, argTypes, + null); } } else { SqlCall testCall = @@ -1513,10 +1514,9 @@ public class SqlValidatorImpl implements SqlValidatorWithHints { return type; } - public CalciteException handleUnresolvedFunction( - SqlCall call, - SqlFunction unresolvedFunction, - List<RelDataType> argTypes) { + public CalciteException handleUnresolvedFunction(SqlCall call, + SqlFunction unresolvedFunction, List<RelDataType> argTypes, + List<String> argNames) { // For builtins, we can give a better error message final List<SqlOperator> overloads = Lists.newArrayList(); opTab.lookupOperatorOverloads(unresolvedFunction.getNameAsId(), null, @@ -1534,7 +1534,7 @@ public class SqlValidatorImpl implements SqlValidatorWithHints { } AssignableOperandTypeChecker typeChecking = - new AssignableOperandTypeChecker(argTypes); + new AssignableOperandTypeChecker(argTypes, argNames); String signature = typeChecking.getAllowedSignatures( unresolvedFunction, @@ -1623,18 +1623,19 @@ public class SqlValidatorImpl implements SqlValidatorWithHints { returnType); } } else if (node instanceof SqlCall) { - SqlCall call = (SqlCall) node; - SqlOperandTypeInference operandTypeInference = + final SqlCall call = (SqlCall) node; + final SqlOperandTypeInference operandTypeInference = call.getOperator().getOperandTypeInference(); - List<SqlNode> operands = call.getOperandList(); - RelDataType[] operandTypes = new RelDataType[operands.size()]; + final SqlCallBinding callBinding = new SqlCallBinding(this, scope, call); + final List<SqlNode> operands = callBinding.operands(); + final RelDataType[] operandTypes = new RelDataType[operands.size()]; if (operandTypeInference == null) { // TODO: eventually should assert(operandTypeInference != null) // instead; for now just eat it Arrays.fill(operandTypes, unknownType); } else { operandTypeInference.inferOperandTypes( - new SqlCallBinding(this, scope, call), + callBinding, inferredType, operandTypes); } @@ -3982,10 +3983,8 @@ public class SqlValidatorImpl implements SqlValidatorWithHints { // For example, "LOCALTIME()" is illegal. (It should be // "LOCALTIME", which would have been handled as a // SqlIdentifier.) - throw handleUnresolvedFunction( - call, - (SqlFunction) operator, - ImmutableList.<RelDataType>of()); + throw handleUnresolvedFunction(call, (SqlFunction) operator, + ImmutableList.<RelDataType>of(), null); } SqlValidatorScope operandScope = scope.getOperandScope(call);
http://git-wip-us.apache.org/repos/asf/calcite/blob/d012b245/core/src/main/java/org/apache/calcite/sql2rel/SqlToRelConverter.java ---------------------------------------------------------------------- diff --git a/core/src/main/java/org/apache/calcite/sql2rel/SqlToRelConverter.java b/core/src/main/java/org/apache/calcite/sql2rel/SqlToRelConverter.java index a3d9ff7..b826cea 100644 --- a/core/src/main/java/org/apache/calcite/sql2rel/SqlToRelConverter.java +++ b/core/src/main/java/org/apache/calcite/sql2rel/SqlToRelConverter.java @@ -88,6 +88,7 @@ import org.apache.calcite.sql.SemiJoinType; import org.apache.calcite.sql.SqlAggFunction; import org.apache.calcite.sql.SqlBasicCall; import org.apache.calcite.sql.SqlCall; +import org.apache.calcite.sql.SqlCallBinding; import org.apache.calcite.sql.SqlDataTypeSpec; import org.apache.calcite.sql.SqlDelete; import org.apache.calcite.sql.SqlDynamicParam; @@ -4173,7 +4174,8 @@ public class SqlToRelConverter { return agg.lookupAggregates(call); } } - return exprConverter.convertCall(this, call); + return exprConverter.convertCall(this, + new SqlCallBinding(validator, scope, call).permutedCall()); } // implement SqlVisitor http://git-wip-us.apache.org/repos/asf/calcite/blob/d012b245/core/src/main/java/org/apache/calcite/util/Compatible.java ---------------------------------------------------------------------- diff --git a/core/src/main/java/org/apache/calcite/util/Compatible.java b/core/src/main/java/org/apache/calcite/util/Compatible.java index 2d1f1fd..f05e5f3 100644 --- a/core/src/main/java/org/apache/calcite/util/Compatible.java +++ b/core/src/main/java/org/apache/calcite/util/Compatible.java @@ -21,6 +21,7 @@ import com.google.common.collect.ImmutableSortedMap; import com.google.common.collect.ImmutableSortedSet; import com.google.common.collect.Maps; +import java.lang.reflect.Array; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; @@ -60,6 +61,9 @@ public interface Compatible { @Deprecated // to be removed before 2.0 void setSchema(Connection connection, String schema); + /** Calls {@link Method}.{@code getParameters()[i].getName()}. */ + String getParameterName(Method method, int i); + /** Creates the implementation of Compatible suitable for the * current environment. */ class Factory { @@ -102,6 +106,20 @@ public interface Compatible { connection.getClass().getMethod("setSchema", String.class); return method1.invoke(connection, schema); } + if (method.getName().equals("getParameterName")) { + final Method m = (Method) args[0]; + final int i = (Integer) args[1]; + try { + final Method method1 = + m.getClass().getMethod("getParameters"); + Object parameters = method1.invoke(m); + final Object parameter = Array.get(parameters, i); + final Method method3 = parameter.getClass().getMethod("getName"); + return method3.invoke(parameter); + } catch (NoSuchMethodException e) { + return "arg" + i; + } + } return null; } }); http://git-wip-us.apache.org/repos/asf/calcite/blob/d012b245/core/src/main/java/org/apache/calcite/util/ImmutableIntList.java ---------------------------------------------------------------------- diff --git a/core/src/main/java/org/apache/calcite/util/ImmutableIntList.java b/core/src/main/java/org/apache/calcite/util/ImmutableIntList.java index 8610dea..78b3dbe 100644 --- a/core/src/main/java/org/apache/calcite/util/ImmutableIntList.java +++ b/core/src/main/java/org/apache/calcite/util/ImmutableIntList.java @@ -16,6 +16,8 @@ */ package org.apache.calcite.util; +import org.apache.calcite.linq4j.function.Function1; +import org.apache.calcite.linq4j.function.Functions; import org.apache.calcite.runtime.FlatLists; import org.apache.calcite.util.mapping.Mappings; @@ -24,7 +26,6 @@ import com.google.common.collect.Lists; import com.google.common.collect.UnmodifiableListIterator; import java.lang.reflect.Array; -import java.util.AbstractList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; @@ -236,15 +237,14 @@ public class ImmutableIntList extends FlatLists.AbstractFlatList<Integer> { * * <p>For example, {@code range(1, 3)} contains [1, 2]. */ public static List<Integer> range(final int lower, final int upper) { - return new AbstractList<Integer>() { - @Override public Integer get(int index) { - return lower + index; - } - - @Override public int size() { - return upper - lower; - } - }; + return Functions.generate(upper - lower, + new Function1<Integer, Integer>() { + /** @see Bug#upgrade(String) Upgrade to {@code IntFunction} when we + * drop support for JDK 1.7 */ + public Integer apply(Integer index) { + return lower + index; + } + }); } /** Returns the identity list [0, ..., count - 1]. http://git-wip-us.apache.org/repos/asf/calcite/blob/d012b245/core/src/main/java/org/apache/calcite/util/ReflectUtil.java ---------------------------------------------------------------------- diff --git a/core/src/main/java/org/apache/calcite/util/ReflectUtil.java b/core/src/main/java/org/apache/calcite/util/ReflectUtil.java index 4a29253..51b1e6b 100644 --- a/core/src/main/java/org/apache/calcite/util/ReflectUtil.java +++ b/core/src/main/java/org/apache/calcite/util/ReflectUtil.java @@ -16,8 +16,11 @@ */ package org.apache.calcite.util; +import org.apache.calcite.linq4j.function.Parameter; + import com.google.common.collect.ImmutableList; +import java.lang.annotation.Annotation; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.nio.ByteBuffer; @@ -38,7 +41,7 @@ public abstract class ReflectUtil { private static Map<Class, Method> primitiveToByteBufferWriteMethod; static { - primitiveToBoxingMap = new HashMap<Class, Class>(); + primitiveToBoxingMap = new HashMap<>(); primitiveToBoxingMap.put(Boolean.TYPE, Boolean.class); primitiveToBoxingMap.put(Byte.TYPE, Byte.class); primitiveToBoxingMap.put(Character.TYPE, Character.class); @@ -48,11 +51,10 @@ public abstract class ReflectUtil { primitiveToBoxingMap.put(Long.TYPE, Long.class); primitiveToBoxingMap.put(Short.TYPE, Short.class); - primitiveToByteBufferReadMethod = new HashMap<Class, Method>(); - primitiveToByteBufferWriteMethod = new HashMap<Class, Method>(); + primitiveToByteBufferReadMethod = new HashMap<>(); + primitiveToByteBufferWriteMethod = new HashMap<>(); Method[] methods = ByteBuffer.class.getDeclaredMethods(); - for (int i = 0; i < methods.length; ++i) { - Method method = methods[i]; + for (Method method : methods) { Class[] paramTypes = method.getParameterTypes(); if (method.getName().startsWith("get")) { if (!method.getReturnType().isPrimitive()) { @@ -62,8 +64,7 @@ public abstract class ReflectUtil { continue; } primitiveToByteBufferReadMethod.put( - method.getReturnType(), - method); + method.getReturnType(), method); // special case for Boolean: treat as byte if (method.getReturnType().equals(Byte.TYPE)) { @@ -325,7 +326,7 @@ public abstract class ReflectUtil { // the original visiteeClass has a diamond-shaped interface inheritance // graph. (This is common, for example, in JMI.) The idea is to avoid // iterating over a single interface's method more than once in a call. - Map<Class<?>, Method> cache = new HashMap<Class<?>, Method>(); + Map<Class<?>, Method> cache = new HashMap<>(); return lookupVisitMethod( visitorClass, @@ -375,14 +376,9 @@ public abstract class ReflectUtil { } Class<?>[] interfaces = visiteeClass.getInterfaces(); - for (int i = 0; i < interfaces.length; ++i) { - Method method = - lookupVisitMethod( - visitorClass, - interfaces[i], - visitMethodName, - paramTypes, - cache); + for (Class<?> anInterface : interfaces) { + final Method method = lookupVisitMethod(visitorClass, anInterface, + visitMethodName, paramTypes, cache); if (method != null) { if (candidateMethod != null) { if (!method.equals(candidateMethod)) { @@ -427,8 +423,7 @@ public abstract class ReflectUtil { assert ReflectiveVisitor.class.isAssignableFrom(visitorBaseClazz); assert Object.class.isAssignableFrom(visiteeBaseClazz); return new ReflectiveVisitDispatcher<R, E>() { - final Map<List<Object>, Method> map = - new HashMap<List<Object>, Method>(); + final Map<List<Object>, Method> map = new HashMap<>(); public Method lookupVisitMethod( Class<? extends R> visitorClass, @@ -535,13 +530,8 @@ public abstract class ReflectUtil { try { final Object o = method.invoke(visitor, args); return returnClazz.cast(o); - } catch (IllegalAccessException e) { - throw Util.newInternal( - e, - "While invoking method '" + method + "'"); - } catch (InvocationTargetException e) { - throw Util.newInternal( - e, + } catch (IllegalAccessException | InvocationTargetException e) { + throw Util.newInternal(e, "While invoking method '" + method + "'"); } } @@ -557,8 +547,7 @@ public abstract class ReflectUtil { methodName, otherArgClassList); if (method == null) { - List<Class> classList = - new ArrayList<Class>(); + List<Class> classList = new ArrayList<>(); classList.add(arg0Clazz); classList.addAll(otherArgClassList); throw new IllegalArgumentException("Method not found: " + methodName @@ -569,6 +558,26 @@ public abstract class ReflectUtil { }; } + /** Derives the name of the {@code i}th parameter of a method. */ + public static String getParameterName(Method method, int i) { + for (Annotation annotation : method.getParameterAnnotations()[i]) { + if (annotation.annotationType() == Parameter.class) { + return ((Parameter) annotation).name(); + } + } + return Compatible.INSTANCE.getParameterName(method, i); + } + + /** Derives whether the {@code i}th parameter of a method is optional. */ + public static boolean isParameterOptional(Method method, int i) { + for (Annotation annotation : method.getParameterAnnotations()[i]) { + if (annotation.annotationType() == Parameter.class) { + return ((Parameter) annotation).optional(); + } + } + return false; + } + //~ Inner Classes ---------------------------------------------------------- /** http://git-wip-us.apache.org/repos/asf/calcite/blob/d012b245/core/src/main/java/org/apache/calcite/util/mapping/Mappings.java ---------------------------------------------------------------------- diff --git a/core/src/main/java/org/apache/calcite/util/mapping/Mappings.java b/core/src/main/java/org/apache/calcite/util/mapping/Mappings.java index dcacb56..c53d300 100644 --- a/core/src/main/java/org/apache/calcite/util/mapping/Mappings.java +++ b/core/src/main/java/org/apache/calcite/util/mapping/Mappings.java @@ -399,6 +399,17 @@ public abstract class Mappings { return new Permutation(IntList.toArray(targets)); } + /** Creates a bijection. + * + * <p>Throws if sources and targets are not one to one. */ + public static Mapping bijection(Map<Integer, Integer> targets) { + final List<Integer> targetList = new ArrayList<>(); + for (int i = 0; i < targets.size(); i++) { + targetList.add(targets.get(i)); + } + return new Permutation(IntList.toArray(targetList)); + } + /** * Returns whether a mapping is the identity. */ http://git-wip-us.apache.org/repos/asf/calcite/blob/d012b245/core/src/main/resources/org/apache/calcite/runtime/CalciteResource.properties ---------------------------------------------------------------------- diff --git a/core/src/main/resources/org/apache/calcite/runtime/CalciteResource.properties b/core/src/main/resources/org/apache/calcite/runtime/CalciteResource.properties index 9703e1d..9787ba9 100644 --- a/core/src/main/resources/org/apache/calcite/runtime/CalciteResource.properties +++ b/core/src/main/resources/org/apache/calcite/runtime/CalciteResource.properties @@ -132,6 +132,9 @@ OrderByOverlap=ORDER BY not allowed in both base and referenced windows RefWindowWithFrame=Referenced window cannot have framing declarations TypeNotSupported=Type ''{0}'' is not supported FunctionQuantifierNotAllowed=DISTINCT/ALL not allowed with {0} function +SomeButNotAllArgumentsAreNamed=Some but not all arguments are named +DuplicateArgumentName=Duplicate argument name ''{0}'' +DefaultForOptionalParameter=DEFAULT is only allowed for optional parameters AccessNotAllowed=Not allowed to perform {0} on {1} MinMaxBadType=The {0} function does not support the {1} data type. OnlyScalarSubqueryAllowed=Only scalar subqueries allowed in select list. http://git-wip-us.apache.org/repos/asf/calcite/blob/d012b245/core/src/test/java/org/apache/calcite/sql/parser/SqlParserTest.java ---------------------------------------------------------------------- diff --git a/core/src/test/java/org/apache/calcite/sql/parser/SqlParserTest.java b/core/src/test/java/org/apache/calcite/sql/parser/SqlParserTest.java index 4060bc9..80930ba 100644 --- a/core/src/test/java/org/apache/calcite/sql/parser/SqlParserTest.java +++ b/core/src/test/java/org/apache/calcite/sql/parser/SqlParserTest.java @@ -719,6 +719,40 @@ public class SqlParserTest { checkExp("ln(power(2,2))", "LN(POWER(2, 2))"); } + @Test public void testFunctionNamedArgument() { + checkExp("foo(x => 1)", + "`FOO`(`X` => 1)"); + checkExp("foo(x => 1, \"y\" => 'a', z => x <= y)", + "`FOO`(`X` => 1, `y` => 'a', `Z` => (`X` <= `Y`))"); + checkExpFails("foo(x.y ^=>^ 1)", + "(?s).*Encountered \"=>\" at .*"); + checkExpFails("foo(a => 1, x.y ^=>^ 2, c => 3)", + "(?s).*Encountered \"=>\" at .*"); + } + + @Test public void testFunctionDefaultArgument() { + checkExp("foo(1, DEFAULT, default, 'default', \"default\", 3)", + "`FOO`(1, DEFAULT, DEFAULT, 'default', `default`, 3)"); + checkExp("foo(DEFAULT)", + "`FOO`(DEFAULT)"); + checkExp("foo(x => 1, DEFAULT)", + "`FOO`(`X` => 1, DEFAULT)"); + checkExp("foo(y => DEFAULT, x => 1)", + "`FOO`(`Y` => DEFAULT, `X` => 1)"); + checkExp("foo(x => 1, y => DEFAULT)", + "`FOO`(`X` => 1, `Y` => DEFAULT)"); + check("select sum(DISTINCT DEFAULT) from t group by x", + "SELECT SUM(DISTINCT DEFAULT)\n" + + "FROM `T`\n" + + "GROUP BY `X`"); + checkExpFails("foo(x ^+^ DEFAULT)", + "(?s).*Encountered \"\\+ DEFAULT\" at .*"); + checkExpFails("foo(0, x ^+^ DEFAULT + y)", + "(?s).*Encountered \"\\+ DEFAULT\" at .*"); + checkExpFails("foo(0, DEFAULT ^+^ y)", + "(?s).*Encountered \"\\+\" at .*"); + } + @Test public void testAggregateFilter() { sql("select sum(sal) filter (where gender = 'F') as femaleSal,\n" + " sum(sal) filter (where true) allSal,\n" http://git-wip-us.apache.org/repos/asf/calcite/blob/d012b245/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 24dc2ce..96b97a1 100644 --- a/core/src/test/java/org/apache/calcite/test/JdbcTest.java +++ b/core/src/test/java/org/apache/calcite/test/JdbcTest.java @@ -45,6 +45,7 @@ import org.apache.calcite.linq4j.Queryable; import org.apache.calcite.linq4j.function.Function0; import org.apache.calcite.linq4j.function.Function1; import org.apache.calcite.linq4j.function.Function2; +import org.apache.calcite.linq4j.function.Parameter; import org.apache.calcite.linq4j.tree.Types; import org.apache.calcite.plan.RelOptCluster; import org.apache.calcite.plan.RelOptTable; @@ -1591,7 +1592,7 @@ public class JdbcTest { + "c0=Store 15; c1=1997; m0=52644.0700\n" + "c0=Store 11; c1=1997; m0=55058.7900\n", "select \"customer\".\"yearly_income\" as \"c0\"," - + " \"customer\".\"education\" as \"c1\" \n" + + " \"customer\".\"education\" as \"c1\"\n" + "from \"customer\" as \"customer\",\n" + " \"sales_fact_1997\" as \"sales_fact_1997\"\n" + "where \"sales_fact_1997\".\"customer_id\" = \"customer\".\"customer_id\"\n" @@ -2018,7 +2019,7 @@ public class JdbcTest { final StringBuilder buf = new StringBuilder(); buf.append("select count(*)"); for (int i = 0; i < n; i++) { - buf.append(i == 0 ? "\nfrom " : ",\n ") + buf.append(i == 0 ? "\nfrom " : ",\n") .append("\"hr\".\"depts\" as d").append(i); } for (int i = 1; i < n; i++) { @@ -5265,6 +5266,18 @@ public class JdbcTest { + "'\n" + " },\n" + " {\n" + + " name: 'MY_LEFT',\n" + + " className: '" + + MyLeftFunction.class.getName() + + "'\n" + + " },\n" + + " {\n" + + " name: 'ABCDE',\n" + + " className: '" + + MyAbcdeFunction.class.getName() + + "'\n" + + " },\n" + + " {\n" + " name: 'MY_STR',\n" + " className: '" + MyToStringFunction.class.getName() @@ -5436,6 +5449,78 @@ public class JdbcTest { .returns("P0=0; P1=1; P2=2\n"); } + /** Tests passing parameters to user-defined function by name. */ + @Test public void testUdfArgumentName() { + final CalciteAssert.AssertThat with = withUdf(); + // arguments in physical order + with.query("values (\"adhoc\".my_left(\"s\" => 'hello', \"n\" => 3))") + .returns("EXPR$0=hel\n"); + // arguments in reverse order + with.query("values (\"adhoc\".my_left(\"n\" => 3, \"s\" => 'hello'))") + .returns("EXPR$0=hel\n"); + with.query("values (\"adhoc\".my_left(\"n\" => 1 + 2, \"s\" => 'hello'))") + .returns("EXPR$0=hel\n"); + // duplicate argument names + with.query("values (\"adhoc\".my_left(\"n\" => 3, \"n\" => 2, \"s\" => 'hello'))") + .throws_("Duplicate argument name 'n'\n"); + // invalid argument names + with.query("values (\"adhoc\".my_left(\"n\" => 3, \"m\" => 2, \"s\" => 'h'))") + .throws_("No match found for function signature " + + "MY_LEFT(n => <NUMERIC>, m => <NUMERIC>, s => <CHARACTER>)\n"); + // missing arguments + with.query("values (\"adhoc\".my_left(\"n\" => 3))") + .throws_("No match found for function signature MY_LEFT(n => <NUMERIC>)\n"); + with.query("values (\"adhoc\".my_left(\"s\" => 'hello'))") + .throws_("No match found for function signature MY_LEFT(s => <CHARACTER>)\n"); + // arguments of wrong type + with.query("values (\"adhoc\".my_left(\"n\" => 'hello', \"s\" => 'x'))") + .throws_("No match found for function signature " + + "MY_LEFT(n => <CHARACTER>, s => <CHARACTER>)\n"); + with.query("values (\"adhoc\".my_left(\"n\" => 1, \"s\" => 0))") + .throws_("No match found for function signature " + + "MY_LEFT(n => <NUMERIC>, s => <NUMERIC>)\n"); + } + + /** Tests calling a user-defined function some of whose parameters are + * optional. */ + @Test public void testUdfArgumentOptional() { + final CalciteAssert.AssertThat with = withUdf(); + with.query("values (\"adhoc\".abcde(a=>1,b=>2,c=>3,d=>4,e=>5))") + .returns("EXPR$0={a: 1, b: 2, c: 3, d: 4, e: 5}\n"); + with.query("values (\"adhoc\".abcde(1,2,3,4,CAST(NULL AS INTEGER)))") + .returns("EXPR$0={a: 1, b: 2, c: 3, d: 4, e: null}\n"); + with.query("values (\"adhoc\".abcde(a=>1,b=>2,c=>3,d=>4))") + .returns("EXPR$0={a: 1, b: 2, c: 3, d: 4, e: null}\n"); + with.query("values (\"adhoc\".abcde(a=>1,b=>2,c=>3))") + .returns("EXPR$0={a: 1, b: 2, c: 3, d: null, e: null}\n"); + with.query("values (\"adhoc\".abcde(a=>1,e=>5,c=>3))") + .returns("EXPR$0={a: 1, b: null, c: 3, d: null, e: 5}\n"); + with.query("values (\"adhoc\".abcde(1,2,3))") + .returns("EXPR$0={a: 1, b: 2, c: 3, d: null, e: null}\n"); + with.query("values (\"adhoc\".abcde(1,2,3,4))") + .returns("EXPR$0={a: 1, b: 2, c: 3, d: 4, e: null}\n"); + with.query("values (\"adhoc\".abcde(1,2,3,4,5))") + .returns("EXPR$0={a: 1, b: 2, c: 3, d: 4, e: 5}\n"); + with.query("values (\"adhoc\".abcde(1,2))") + .throws_("No match found for function signature ABCDE(<NUMERIC>, <NUMERIC>)"); + with.query("values (\"adhoc\".abcde(1,DEFAULT,3))") + .returns("EXPR$0={a: 1, b: null, c: 3, d: null, e: null}\n"); + with.query("values (\"adhoc\".abcde(1,DEFAULT,'abcde'))") + .throws_("No match found for function signature ABCDE(<NUMERIC>, <ANY>, <CHARACTER>)"); + with.query("values (\"adhoc\".abcde(true))") + .throws_("No match found for function signature ABCDE(<BOOLEAN>)"); + with.query("values (\"adhoc\".abcde(true,DEFAULT))") + .throws_("No match found for function signature ABCDE(<BOOLEAN>, <ANY>)"); + with.query("values (\"adhoc\".abcde(1,DEFAULT,3,DEFAULT))") + .returns("EXPR$0={a: 1, b: null, c: 3, d: null, e: null}\n"); + with.query("values (\"adhoc\".abcde(1,2,DEFAULT))") + .throws_("DEFAULT is only allowed for optional parameters"); + with.query("values (\"adhoc\".abcde(a=>1,b=>2,c=>DEFAULT))") + .throws_("DEFAULT is only allowed for optional parameters"); + with.query("values (\"adhoc\".abcde(a=>1,b=>DEFAULT,c=>3))") + .returns("EXPR$0={a: 1, b: null, c: 3, d: null, e: null}\n"); + } + /** Test for * {@link org.apache.calcite.runtime.CalciteResource#requireDefaultConstructor(String)}. */ @Test public void testUserDefinedFunction2() throws Exception { @@ -6214,10 +6299,10 @@ public class JdbcTest { @Test public void testLexCaseInsensitiveSubQueryField() { CalciteAssert.that() .with(Lex.MYSQL) - .query("select DID \n" - + "from (select deptid as did \n" + .query("select DID\n" + + "from (select deptid as did\n" + " FROM\n" - + " ( values (1), (2) ) as T1(deptid) \n" + + " ( values (1), (2) ) as T1(deptid)\n" + " ) ") .returnsUnordered("DID=1", "DID=2"); } @@ -6395,11 +6480,11 @@ public class JdbcTest { CalciteAssert.that() .with(Lex.ORACLE); - with.query("select DID from (select DEPTID as did FROM \n " + with.query("select DID from (select DEPTID as did FROM\n" + " ( values (1), (2) ) as T1(deptid) ) ") .returnsUnordered("DID=1", "DID=2"); - with.query("select x.DID from (select DEPTID as did FROM \n " + with.query("select x.DID from (select DEPTID as did FROM\n" + " ( values (1), (2) ) as T1(deptid) ) X") .returnsUnordered("DID=1", "DID=2"); } @@ -6409,23 +6494,23 @@ public class JdbcTest { CalciteAssert.that() .with(Lex.MYSQL); - with.query("select DID from (select deptid as did FROM \n " + with.query("select DID from (select deptid as did FROM\n" + " ( values (1), (2) ) as T1(deptid) ) ") .returnsUnordered("DID=1", "DID=2"); - with.query("select x.DID from (select deptid as did FROM \n " + with.query("select x.DID from (select deptid as did FROM\n" + " ( values (1), (2) ) as T1(deptid) ) X ") .returnsUnordered("DID=1", "DID=2"); - with.query("select X.DID from (select deptid as did FROM \n " + with.query("select X.DID from (select deptid as did FROM\n" + " ( values (1), (2) ) as T1(deptid) ) X ") .returnsUnordered("DID=1", "DID=2"); - with.query("select X.DID2 from (select deptid as did FROM \n " + with.query("select X.DID2 from (select deptid as did FROM\n" + " ( values (1), (2) ) as T1(deptid) ) X (DID2)") .returnsUnordered("DID2=1", "DID2=2"); - with.query("select X.DID2 from (select deptid as did FROM \n " + with.query("select X.DID2 from (select deptid as did FROM\n" + " ( values (1), (2) ) as T1(deptid) ) X (DID2)") .returnsUnordered("DID2=1", "DID2=2"); } @@ -6435,23 +6520,23 @@ public class JdbcTest { CalciteAssert.that() .with(Lex.MYSQL); - with.query("select `DID` from (select deptid as did FROM \n " + with.query("select `DID` from (select deptid as did FROM\n" + " ( values (1), (2) ) as T1(deptid) ) ") .returnsUnordered("DID=1", "DID=2"); - with.query("select `x`.`DID` from (select deptid as did FROM \n " + with.query("select `x`.`DID` from (select deptid as did FROM\n" + " ( values (1), (2) ) as T1(deptid) ) X ") .returnsUnordered("DID=1", "DID=2"); - with.query("select `X`.`DID` from (select deptid as did FROM \n " + with.query("select `X`.`DID` from (select deptid as did FROM\n" + " ( values (1), (2) ) as T1(deptid) ) X ") .returnsUnordered("DID=1", "DID=2"); - with.query("select `X`.`DID2` from (select deptid as did FROM \n " + with.query("select `X`.`DID2` from (select deptid as did FROM\n" + " ( values (1), (2) ) as T1(deptid) ) X (DID2)") .returnsUnordered("DID2=1", "DID2=2"); - with.query("select `X`.`DID2` from (select deptid as did FROM \n " + with.query("select `X`.`DID2` from (select deptid as did FROM\n" + " ( values (1), (2) ) as T1(deptid) ) X (DID2)") .returnsUnordered("DID2=1", "DID2=2"); } @@ -6459,7 +6544,7 @@ public class JdbcTest { @Test public void testUnquotedCaseSensitiveSubQuerySqlServer() { CalciteAssert.that() .with(Lex.SQL_SERVER) - .query("select DID from (select deptid as did FROM \n " + .query("select DID from (select deptid as did FROM\n" + " ( values (1), (2) ) as T1(deptid) ) ") .returnsUnordered("DID=1", "DID=2"); } @@ -6467,7 +6552,7 @@ public class JdbcTest { @Test public void testQuotedCaseSensitiveSubQuerySqlServer() { CalciteAssert.that() .with(Lex.SQL_SERVER) - .query("select [DID] from (select deptid as did FROM \n " + .query("select [DID] from (select deptid as did FROM\n" + " ( values (1), (2) ) as T1([deptid]) ) ") .returnsUnordered("DID=1", "DID=2"); } @@ -6976,16 +7061,37 @@ public class JdbcTest { public MyTable2[] mytable2 = { new MyTable2() }; } - /** Example of a UDF with a non-static {@code eval} method. */ + /** Example of a UDF with a non-static {@code eval} method, + * and named parameters. */ public static class MyPlusFunction { - public int eval(int x, int y) { + public int eval(@Parameter(name = "x") int x, @Parameter(name = "y") int y) { return x + y; } } + /** Example of a UDF with named parameters. */ + public static class MyLeftFunction { + public String eval(@Parameter(name = "s") String s, + @Parameter(name = "n") int n) { + return s.substring(0, n); + } + } + + /** Example of a UDF with named parameters, some of them optional. */ + public static class MyAbcdeFunction { + public String eval(@Parameter(name = "A", optional = false) Integer a, + @Parameter(name = "B", optional = true) Integer b, + @Parameter(name = "C", optional = false) Integer c, + @Parameter(name = "D", optional = true) Integer d, + @Parameter(name = "E", optional = true) Integer e) { + return "{a: " + a + ", b: " + b + ", c: " + c + ", d: " + d + ", e: " + + e + "}"; + } + } + /** Example of a non-strict UDF. (Does something useful when passed NULL.) */ public static class MyToStringFunction { - public static String eval(Object o) { + public static String eval(@Parameter(name = "o") Object o) { if (o == null) { return "<null>"; } http://git-wip-us.apache.org/repos/asf/calcite/blob/d012b245/core/src/test/java/org/apache/calcite/util/UtilTest.java ---------------------------------------------------------------------- diff --git a/core/src/test/java/org/apache/calcite/util/UtilTest.java b/core/src/test/java/org/apache/calcite/util/UtilTest.java index b814976..56336ce 100644 --- a/core/src/test/java/org/apache/calcite/util/UtilTest.java +++ b/core/src/test/java/org/apache/calcite/util/UtilTest.java @@ -21,6 +21,7 @@ import org.apache.calcite.avatica.util.Spaces; import org.apache.calcite.examples.RelBuilderExample; import org.apache.calcite.linq4j.Ord; import org.apache.calcite.linq4j.function.Function1; +import org.apache.calcite.linq4j.function.Parameter; import org.apache.calcite.runtime.FlatLists; import org.apache.calcite.runtime.Resources; import org.apache.calcite.sql.SqlDialect; @@ -42,6 +43,7 @@ import java.io.PrintWriter; import java.io.StringWriter; import java.io.UnsupportedEncodingException; import java.lang.management.MemoryType; +import java.lang.reflect.Method; import java.math.BigDecimal; import java.sql.Timestamp; import java.text.MessageFormat; @@ -1414,6 +1416,17 @@ public class UtilTest { assertThat(reverse.next().e, is("a")); assertThat(reverse.hasNext(), is(false)); } + + /** Tests {@link org.apache.calcite.util.ReflectUtil#getParameterName}. */ + @Test public void testParameterName() throws NoSuchMethodException { + final Method method = UtilTest.class.getMethod("foo", int.class, int.class); + assertThat(ReflectUtil.getParameterName(method, 0), is("arg0")); + assertThat(ReflectUtil.getParameterName(method, 1), is("j")); + } + + /** Dummy method for {@link #testParameterName()} to inspect. */ + public static void foo(int i, @Parameter(name = "j") int j) { + } } // End UtilTest.java http://git-wip-us.apache.org/repos/asf/calcite/blob/d012b245/linq4j/src/main/java/org/apache/calcite/linq4j/function/Functions.java ---------------------------------------------------------------------- diff --git a/linq4j/src/main/java/org/apache/calcite/linq4j/function/Functions.java b/linq4j/src/main/java/org/apache/calcite/linq4j/function/Functions.java index bd7a446..8d6ccbf 100644 --- a/linq4j/src/main/java/org/apache/calcite/linq4j/function/Functions.java +++ b/linq4j/src/main/java/org/apache/calcite/linq4j/function/Functions.java @@ -28,6 +28,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.RandomAccess; /** * Utilities relating to functions. @@ -422,14 +423,7 @@ public abstract class Functions { if (size < 0) { throw new IllegalArgumentException(); } - return new AbstractList<E>() { - public int size() { - return size; - } - public E get(int index) { - return fn.apply(index); - } - }; + return new GeneratingList<>(size, fn); } /** @@ -647,6 +641,26 @@ public abstract class Functions { static final Ignore INSTANCE = new Ignore(); } + + /** List that generates each element using a function. */ + private static class GeneratingList<E> extends AbstractList<E> + implements RandomAccess { + private final int size; + private final Function1<Integer, E> fn; + + public GeneratingList(int size, Function1<Integer, E> fn) { + this.size = size; + this.fn = fn; + } + + public int size() { + return size; + } + + public E get(int index) { + return fn.apply(index); + } + } } // End Functions.java http://git-wip-us.apache.org/repos/asf/calcite/blob/d012b245/linq4j/src/main/java/org/apache/calcite/linq4j/function/Parameter.java ---------------------------------------------------------------------- diff --git a/linq4j/src/main/java/org/apache/calcite/linq4j/function/Parameter.java b/linq4j/src/main/java/org/apache/calcite/linq4j/function/Parameter.java new file mode 100644 index 0000000..9378bfc --- /dev/null +++ b/linq4j/src/main/java/org/apache/calcite/linq4j/function/Parameter.java @@ -0,0 +1,86 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to you under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.calcite.linq4j.function; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotation that supplies metadata about a function parameter. + * + * <p>A typical use is to derive names for the parameters of user-defined + * functions. + * + * <p>Here is an example: + * + * <blockquote><pre> + * public static class MyLeftFunction { + * public String eval( + * @Parameter(name = "s") String s, + * @Parameter(name = "n", optional = true) Integer n) { + * return s.substring(0, n == null ? 1 : n); + * } + * }</pre></blockquote> + * + * <p>The first parameter is named "s" and is mandatory, + * and the second parameter is named "n" and is optional. + * + * <p>If this annotation is present, it supersedes information that might be + * available via + * {@code Executable.getParameters()} (JDK 1.8 and above). + * + * <p>If the annotation is not specified, parameters will be named "arg0", + * "arg1" et cetera, and will be mandatory. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.PARAMETER }) +public @interface Parameter { + /** The name of the parameter. + * + * <p>The name is used when the function is called + * with parameter assignment, for example {@code foo(x => 1, y => 'a')}. + */ + String name(); + + /** Returns whether the parameter is optional. + * + * <p>An optional parameter does not need to be specified when you call the + * function. + * + * <p>If you call a function using positional parameter syntax, you can omit + * optional parameters on the trailing edge. For example, if you have a + * function + * {@code baz(int a, int b optional, int c, int d optional, int e optional)} + * then you can call {@code baz(a, b, c, d, e)} + * or {@code baz(a, b, c, d)} + * or {@code baz(a, b, c)} + * but not {@code baz(a, b)} because {@code c} is not optional. + * + * <p>If you call a function using parameter name assignment syntax, you can + * omit any parameter that has a default value. For example, you can call + * {@code baz(a => 1, e => 5, c => 3)}, omitting optional parameters {@code b} + * and {@code d}. + * + * <p>Currently, the default value used when a parameter is not specified + * is NULL, and therefore optional parameters must be nullable. + */ + boolean optional() default false; +} + +// End Parameter.java http://git-wip-us.apache.org/repos/asf/calcite/blob/d012b245/site/_docs/reference.md ---------------------------------------------------------------------- diff --git a/site/_docs/reference.md b/site/_docs/reference.md index 7df7926..bb594ac 100644 --- a/site/_docs/reference.md +++ b/site/_docs/reference.md @@ -594,3 +594,96 @@ Not implemented: | GROUPING(expression) | Returns 1 if expression is rolled up in the current row's grouping set, 0 otherwise | GROUP_ID() | Returns an integer that uniquely identifies the combination of grouping keys | GROUPING_ID(expression [, expression ] * ) | Returns a bit vector of the given grouping expressions + +### User-defined functions + +Calcite is extensible. You can define each kind of function using user code. +For each kind of function there are often several ways to define a function, +varying from convenient to efficient. + +To implement a *scalar function*, there are 3 options: + +* Create a class with a public static `eval` method. and register the class; +* Create a class with a public non-static `eval` method. and a public + constructor with no arguments, and register the class; +* Create a class with one or more public static methods, and register + each class and method. + +To implement an *aggregate function*: + +* Create a class with public static `init`, `add` and `result` methods, and + register the class; +* Create a class with public non-static `init`, `add` and `result` methods, and + a public constructor with no arguments, and register the class. + +Optionally, add a public `merge` method to the class; this allows Calcite to +generate code that merges sub-totals. + +Optionally, make your class implement the +[SqlSplittableAggFunction]({{ site.apiRoot }}/org/apache/calcite/sql/SqlSplittableAggFunction.html) +interface; this allows Calcite to decompose the function across several stages +of aggregation, roll up from summary tables, and push it through joins. + +To implement a *table function*: + +* Create a class with a static `eval` method that returns + [TranslatableTable]({{ site.apiRoot }}/org/apache/calcite/schema/TranslatableTable.html) + or + [QueryableTable]({{ site.apiRoot }}/org/apache/calcite/schema/QueryableTable.html), + and register the class; +* Create a class with a non-static `eval` method that returns + [TranslatableTable]({{ site.apiRoot }}/org/apache/calcite/schema/TranslatableTable.html) + or + [QueryableTable]({{ site.apiRoot }}/org/apache/calcite/schema/QueryableTable.html), + and register the class. + +Calcite deduces the parameter types and result type of a function from the +parameter and return types of the Java method that implements it. Further, you +can specify the name and optionality of each parameter using the +[Parameter]({{ site.apiRoot }}/org/apache/calcite/linq4j/function/Parameter.html) +annotation. + +### Calling functions with named and optional parameters + +Usually when you call a function, you need to specify all of its parameters, +in order. But if the function has been defined with named and optional +parameters + +Suppose you have a function `f`, declared as in the following pseudo syntax: + +```FUNCTION f( + INTEGER a, + INTEGER b DEFAULT NULL, + INTEGER c, + INTEGER d DEFAULT NULL, + INTEGER e DEFAULT NULL) RETURNS INTEGER``` + +Note that all parameters have names, and parameters `b`, `d` and `e` +have a default value of `NULL` and are therefore optional. +(In Calcite, `NULL` is the only allowable default value for optional parameters; +this may change +[in future](https://issues.apache.org/jira/browse/CALCITE-947).) + +You can omit optional arguments at the end of the list, or use the `DEFAULT` +keyword for any optional arguments. +Here are some examples: + +* `f(1, 2, 3, 4, 5)` provides a value to each parameter, in order; +* `f(1, 2, 3, 4)` omits `e`, which gets its default value, `NULL`; +* `f(1, DEFAULT, 3)` omits `d` and `e`, + and specifies to use the default value of `b`; +* `f(1, DEFAULT, 3, DEFAULT, DEFAULT)` has the same effect as the previous + example; +* `f(1, 2)` is not legal, because `c` is not optional; +* `f(1, 2, DEFAULT, 4)` is not legal, because `c` is not optional. + +You can specify arguments by name using the `=>` syntax. +If one argument is named, they all must be. +Arguments may be in any other, but must not specify any argument more than once, +and you need to provide a value for every parameter which is not optional. +Here are some examples: + +* `f(c => 3, d => 1, a => 0)` is equivalent to `f(0, NULL, 3, 1, NULL)`; +* `f(c => 3, d => 1)` is not legal, because you have not specified a value for + `a` and `a` is not optional. +
