Presto 标量函数注册和调用过程简述
在Presto 函数开发一文中已经介绍过如何进行函数开发,本文主要讲述标量函数(Scalar Function)实现之后,是如何在Presto内部进行注册和被调用的。主要讲述标量函数是因为:三类函数的注册和调用过程略有不同,而实际查询中调用最多的是标量函数。
标量函数注册
函数在能够调用之前,首先要进行注册,上一篇文章已经介绍过函数注册的方法,那么函数在注册时究竟注册了哪些信息呢?函数注册实际上是维护FunctinoRegistry类中的一个 MultiMap,Key 为函数的限定名(QualifiedName,可以简单地理解为函数名),Value 为SqlFunction
接口的实现类,实际主要为SqlAggregationFunction
、SqlWindowFunction
和SqlScalarFunction
这三个类的子类。SqlScalarFunction
是一个抽象类,定义如下:
public abstract class SqlScalarFunction
implements SqlFunction
{
private final Signature signature;
protected SqlScalarFunction(Signature signature)
{
this.signature = requireNonNull(signature, "signature is null");
checkArgument(signature.getKind() == SCALAR, "function kind must be SCALAR");
}
@Override
public final Signature getSignature()
{
return signature;
}
public abstract ScalarFunctionImplementation specialize(BoundVariables boundVariables, int arity, TypeManager typeManager, FunctionRegistry functionRegistry);
public static PolymorphicScalarFunctionBuilder builder(Class<?> clazz)
{
return new PolymorphicScalarFunctionBuilder(clazz);
}
}
可以看出,其子类需要获取Signature
和实现specialize
方法。
首先来看Signature
:
public final class Signature
{
private final String name;
private final FunctionKind kind;
private final List<TypeVariableConstraint> typeVariableConstraints;
private final List<LongVariableConstraint> longVariableConstraints;
private final TypeSignature returnType;
private final List<TypeSignature> argumentTypes;
private final boolean variableArity;
....
}
类的成员变量说明如下:
name
:函数名,不包括参数类型和结果类型,例如:函数isnull(T):boolean
的函数名为isnull
kind
:枚举类型,有 SCALAR、AGGREGATE 和 WINDOW三种取值,用于区分函数类型typeVariableConstraints
:类型变量约束,记录函数中的类型变量名,以及类型变量所需要满足的约束条件:类型是否为comparable、orderable 和是否绑定具体类型。例如:contains<T:comparable>(array(T),T):boolean
函数要求类型T
满足comparable
;array_sort<E:orderable>(array(E)):array(E)
函数要求类型E
满足orderable
;判断两个ROW类型是否相等的操作符(操作符也属于标量函数)$operator$EQUAL<T:comparable:row<*>>(T,T):boolean
要求类型T
为ROW类型。LongVariableConstraint
:长整型变量约束,记录函数中带有约束的长整型变量的计算表达式(一般用于计算返回类型中的长整型变量)。例如:函数concat<u:x + y>(char(x),char(y)):char(u)
的返回类型中长整型变量u
的计算表达式为x + y
returnType
:函数的返回类型argumentTypes
:函数参数类型variableArity
:标记是否为变长参数
以上成员变量都可以从函数实现的类对象中,根据注解规则解析获得。除了获取Signature
,由于同一个函数可能会有多个实现(例如上一篇文章介绍的isnull<T>(T):boolean
函数,因为传入的参数类型可能不同,所以有五个实现方法),所以还要记录函数的实现方法。源码中将实现方法分为三类:
exactimplementation
:函数中不包含类型变量,即函数的参数类型和返回类型都是确定的specializedImplementation
:函数中包含类型变量,但类型变量作用在具体的 Java 类型(Native Container Type)上genericImplement
:函数中包含类型变量,但是类型变量作用在 Object 类型上
Presto 保存的是实现方法的MethodHandle
,通过反射获取Method
,再保存Method
对应的MethodHandle
(MethodHandle
在JDK1.7引入,调用的效率比反射高),如果该方法不是静态方法,还要将MethodHandle
的中的this
参数改为Object
来避免调用时的类加载问题。所以,抽象方法specialize
的本质是通过传入的参数,来获取匹配到的MethodHandle
,这部分放到下一节的标量函数调用中进行讲解。
可以看出,标量函数注册的本质是保存函数的Signature
和MethodHandle
。开发者根据注解框架实现的标量函数,注册时再根据注解解析出Signature
和MethodHandle
,封装在ParametricScalar
对象中。当然,开发者也可以自行继承SqlScalarFunction
,自己定义Signature
和实现specialize
方法。
标量函数调用
标量函数调用的入口为InterpretedFunctionInvoker
类的public Object invoke(Signature function, ConnectorSession session, List<Object> arguments)
方法,形参里的Signature
是由语义分析时,根据词法分析得到函数QualifiedName和语法分析得到的参数类型,调用FunctionRegistry
中的public Signature resolveFunction(QualifiedName name, List<TypeSignatureProvider> parameterTypes)
方法得到。所以,标量函数调用的关键是resolveFunction
方法和invoke
方法。
首先来看resolveFunction
方法,该方法主要通过函数名和函数参数类型来确定Signature
,流程如下:
虚线红框中的三个匹配过程实际上是调用了同一个方法:Optional<Signature> matchFunction(Collection<SqlFunction> candidates, List<TypeSignatureProvider> parameters, boolean coercionAllowed)
,其中的coercionAllowed
为是否将实参类型转化为形参类型的标识。matchFunction
方法等价于为Signature
中的变量寻找赋值,不仅要满足变量类型是对应的实际参数类型的超类,而且对应的实际参数还要满足Signature
中声明的变量约束。将形参类型和实参进行绑定时,还会做一些约定性的检查:
- 一个类型不能既赋给类型变量(type parameter),又赋给字面变量(literal parameter,如
varchar(x)
中的x
) - 字面变量不允许跨类型使用
为了便于理解第二个规定,下面例举几个字面变量跨类型使用的例子:
- x 出现在不同的基本类型中:char(x)和varchar(x)
- x 出现在同一种基本类型的不同位置:decimal(x,y) 和 decimal(z,x)
- p 与不同的字面量、类型或者字面变量组合使用:decimal(p,s1) and decimal(p,s2)
还有一个限制是,如果尝试将实际参数类型decimal(1,0)
赋给Signature
中声明的decimal(x,2)
,会失败,但是使用decimal(3,1)
可以赋值成功。因为根据decimal
的定义,precision 必须大于 scale,即x
必须大于2。
经过一系列的规则匹配和变量求解,最终会返回一个具体的函数函数签名,签名中的类型都是具体类型(即不含变量)。比如简单 SQLselect isnull('a')
,最终得到的Signature
是isnull(varchar(1)):boolean
,实参中的类型varchar(1)
赋给了原先注册的isnull<T>(T):boolean
中的类型变量T
。
再来看invoke
方法,该方法首先会根据传入的Signature
调用FunctionRegistry
中的getScalarFunctionImplementation
来获取最终的MethodHandle
,然后使用具体的参数值来进行实际方法的调用(方法中若需要ConnectorSession
,也在此进行注入)。因为函数注册维护的是QualifiedName->SqlFunction
的映射关系,而调用getScalarFunctionImplementation
时传入的Signature
并没有记录变量与实参的绑定关系,所以这里需要再进行一次类型变量的求解,这一步的计算其实是可以避免的,因为在resolveFunction
中其实已经拿到了变量绑定的关系,可以进行复用,所以340版本中已改为传入带绑定关系的FunctionBinding
。函数注册时说明了一个函数可能有多个实现方法,接下来就是根据形参和实参的绑定关系,调用SqlFunction
的specialize
方法进行对应参数的 Java 类型的匹配,按照exactimplementation
类型->specializedImplementation
类型->genericImplement
类型的顺序进行匹配,一旦匹配成功则直接返回匹配到的实现方法,如果方法中需要传入依赖变量,也在此步骤中根据绑定关系对MethodHandle
进行参数值注入。因为对MethodHandle
的反复编译会导致full GC(怀疑是触发了 JVM Bug),所以 Presto 在FunctionRegistry
中为三类函数分别做了个大小为1000,有效时长为1小时的缓存来避免这个问题。
至此,函数的注册和调用的过程已经完成。熟悉这两个过程可以帮助我们在函数开发和调用中快速地定位问题,除此之外,求解Signature
时的类型转换匹配可以作为类型隐式转换的一个入口。