在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
時的型別轉換匹配可以作為型別隱式轉換的一個入口。