0
  • 聊天消息
  • 系統(tǒng)消息
  • 評(píng)論與回復(fù)
登錄后你可以
  • 下載海量資料
  • 學(xué)習(xí)在線(xiàn)課程
  • 觀(guān)看技術(shù)視頻
  • 寫(xiě)文章/發(fā)帖/加入社區(qū)
會(huì)員中心
創(chuàng)作中心

完善資料讓更多小伙伴認(rèn)識(shí)你,還能領(lǐng)取20積分哦,立即完善>

3天內(nèi)不再提示

如何用Rust過(guò)程宏魔法簡(jiǎn)化SQL函數(shù)呢?

jf_wN0SrCdH ? 來(lái)源:RisingWave 社區(qū) ? 2024-01-23 09:43 ? 次閱讀

背景介紹

#[function("length(varchar)->int4")]
pubfnchar_length(s:&str)->i32{
s.chars().count()asi32
}

這是 RisingWave 中一個(gè) SQL 函數(shù)的實(shí)現(xiàn)。只需短短幾行代碼,通過(guò)在 Rust 函數(shù)上加一行過(guò)程宏,我們就把它包裝成了一個(gè) SQL 函數(shù)。

dev=>selectlength('RisingWave');
length
--------
11
(1row)

類(lèi)似的,除了標(biāo)量函數(shù)(Scalar Function),表函數(shù)(Table Function)和聚合函數(shù)(Aggregate Function)也可以用這樣的方法定義。我們甚至可以利用泛型來(lái)同時(shí)定義多種類(lèi)型的重載函數(shù):

#[function("generate_series(int4,int4)->setofint4")]
#[function("generate_series(int8,int8)->setofint8")]
fngenerate_series(start:T,stop:T)->implIterator{
start..=stop
}

#[aggregate("max(int2)->int2",state="ref")]
#[aggregate("max(int4)->int4",state="ref")]
#[aggregate("max(int8)->int8",state="ref")]
fnmax(state:T,input:T)->T{
state.max(input)
}
dev=>selectgenerate_series(1,3);
generate_series
-----------------
1
2
3
(3rows)

dev=>selectmax(x)fromgenerate_series(1,3)t(x);
max
-----
3
(1row)

利用 Rust 過(guò)程宏,我們將函數(shù)實(shí)現(xiàn)背后的瑣碎細(xì)節(jié)隱藏起來(lái),向開(kāi)發(fā)者暴露一個(gè)干凈簡(jiǎn)潔的接口。這樣我們便能夠專(zhuān)注于函數(shù)本身邏輯的實(shí)現(xiàn),從而大幅提高開(kāi)發(fā)和維護(hù)的效率。

而當(dāng)一個(gè)接口足夠簡(jiǎn)單,簡(jiǎn)單到連 ChatGPT 都可以理解時(shí),讓 AI 幫我們寫(xiě)代碼就不再是天方夜譚了。(警告:AI 會(huì)自信地寫(xiě)出 Bug,使用前需要人工 review)

ab4cffee-b938-11ee-8b88-92fbcf53809c.png

ab53644c-b938-11ee-8b88-92fbcf53809c.png

向 GPT 展示一個(gè) SQL 函數(shù)實(shí)現(xiàn)的例子,然后給出一個(gè)新函數(shù)的文檔,讓他生成完整的 Rust 實(shí)現(xiàn)代碼。

在本文中,我們將深度解析 RisingWave 中 #[function] 過(guò)程宏的設(shè)計(jì)目標(biāo)和工作原理。通過(guò)回答以下幾個(gè)問(wèn)題揭開(kāi)過(guò)程宏的魔法面紗:

函數(shù)執(zhí)行的過(guò)程是怎樣的?

為什么選擇使用過(guò)程宏實(shí)現(xiàn)?

這個(gè)宏是如何展開(kāi)的?生成了怎樣的代碼?

利用過(guò)程宏還能實(shí)現(xiàn)哪些高級(jí)需求?

1向量化計(jì)算模型

RisingWave 是一個(gè)支持 SQL 語(yǔ)言的流處理引擎。在內(nèi)部處理數(shù)據(jù)時(shí),它使用基于列式內(nèi)存存儲(chǔ)的向量化計(jì)算模型。在這種模型下,一個(gè)表(Table)的數(shù)據(jù)按列分割,每一列的數(shù)據(jù)連續(xù)存儲(chǔ)在一個(gè)數(shù)組(Array)中。為了便于理解,本文中我們采用列式內(nèi)存的行業(yè)標(biāo)準(zhǔn) Apache Arrow 格式作為示例。下圖是其中一批數(shù)據(jù)(RecordBatch)的內(nèi)存結(jié)構(gòu),RisingWave 的列存結(jié)構(gòu)與之大同小異。

ab5d2b76-b938-11ee-8b88-92fbcf53809c.png

列式內(nèi)存存儲(chǔ)的數(shù)據(jù)結(jié)構(gòu)

在函數(shù)求值時(shí),我們首先把每個(gè)輸入參數(shù)對(duì)應(yīng)的數(shù)據(jù)列合并成一個(gè) RecordBatch,然后依次讀取每一行的數(shù)據(jù),作為參數(shù)調(diào)用函數(shù),最后將函數(shù)返回值壓縮成一個(gè)數(shù)組,作為最終返回結(jié)果。這種一次處理一批數(shù)據(jù)的方式就是向量化計(jì)算。

ab68ae60-b938-11ee-8b88-92fbcf53809c.png

函數(shù)的向量化求值 之所以要這么折騰一圈做列式存儲(chǔ)、向量化求值,本質(zhì)上還是因?yàn)榕幚砟軌蚓鶖偟艨刂七壿嫷拈_(kāi)銷(xiāo),并充分利用現(xiàn)代 CPU 中的緩存局部性和 SIMD 指令等特性,實(shí)現(xiàn)更高的訪(fǎng)存和計(jì)算性能。

我們將上述函數(shù)求值過(guò)程抽象成一個(gè) Rust trait,大概長(zhǎng)這樣:

pubtraitScalarFunction{
///Callthefunctiononeachrowandreturnresultsasanarray.
fneval(&self,input:&RecordBatch)->Result;
}

在實(shí)際查詢(xún)中,多個(gè)函數(shù)嵌套組合成一個(gè)表達(dá)式。例如表達(dá)式 a + b - c等價(jià)于 sub(add(a, b), c)。對(duì)表達(dá)式求值就相當(dāng)于遞歸地對(duì)多個(gè)函數(shù)進(jìn)行求值。這個(gè)表達(dá)式本身也可以看作一個(gè)函數(shù),同樣適用上面的 trait。因此本文中我們不區(qū)分表達(dá)式和標(biāo)量函數(shù)。

2表達(dá)式執(zhí)行的黑白魔法:類(lèi)型體操 vs 代碼生成

接下來(lái)我們討論在 Rust 語(yǔ)言中如何具體實(shí)現(xiàn)表達(dá)式向量化求值。

2.1 我們要實(shí)現(xiàn)什么

回顧上一節(jié)中提到的求值過(guò)程,寫(xiě)成代碼的整體結(jié)構(gòu)是這樣的:

//首先定義好對(duì)每行數(shù)據(jù)的求值函數(shù)
fnadd(a:i32,b:i32)->i32{
a+b
}

//對(duì)于每一種函數(shù),我們需要定義一個(gè)struct
structAdd;

//并為之實(shí)現(xiàn)ScalarFunctiontrait
implScalarFunctionforAdd{
//在此方法中實(shí)現(xiàn)向量化批處理
fneval(&self,input:&RecordBatch)->Result{
//我們拿到一個(gè)RecordBatch,里面包含了若干列,每一列對(duì)應(yīng)一個(gè)輸入?yún)?shù)
//此時(shí)我們拿到的列是Arc,也就是一個(gè)**類(lèi)型擦除**的數(shù)組
leta0:Arc=input.columns(0);
leta1:Arc=input.columns(1);

//我們可以獲取每一列的數(shù)據(jù)類(lèi)型,并驗(yàn)證它符合函數(shù)的要求
ensure!(a0.data_type()==DataType::Int32);
ensure!(a1.data_type()==DataType::Int32);

//然后將它們downcast到具體的數(shù)組類(lèi)型
leta0:&Int32Array=a0.as_any().downcast_ref().context("typemismatch")?;
leta1:&Int32Array=a1.as_any().downcast_ref().context("typemismatch")?;

//在求值前,我們還需要準(zhǔn)備好一個(gè)arraybuilder存儲(chǔ)返回值
letmutbuilder=Int32Builder::with_capacity(input.num_rows());

//此時(shí)我們就可以通過(guò).iter()來(lái)遍歷具體的元素了
for(v0,v1)ina0.iter().zip(a1.iter()){
//這里我們拿到的v0和v1是Option類(lèi)型
//對(duì)于add函數(shù)來(lái)說(shuō)
letres=match(v0,v1){
//只有當(dāng)所有輸入都非空時(shí),函數(shù)才會(huì)被計(jì)算
(Some(v0),Some(v1))=>Some(add(v0,v1)),
//而任何一個(gè)輸入為空會(huì)導(dǎo)致輸出也為空
_=>None,
};
//最后將結(jié)果存入arraybuilder
builder.append_option(res);
}
//返回結(jié)果array
Ok(Arc::new(builder.finish()))
}
}

我們發(fā)現(xiàn),這個(gè)函數(shù)本體的邏輯只需要短短一個(gè) fn 就可以描述:

fnadd(a:i32,b:i32)->i32{
a+b
}

然而,為了支持在列存上進(jìn)行向量化計(jì)算,還需要實(shí)現(xiàn)后面這一大段樣板代碼來(lái)處理瑣碎邏輯。有什么辦法能自動(dòng)生成這坨代碼呢?

2.2 類(lèi)型體操

著名數(shù)據(jù)庫(kù)專(zhuān)家遲先生曾在博文「數(shù)據(jù)庫(kù)表達(dá)式執(zhí)行的黑魔法:用 Rust 做類(lèi)型體操[1]」中討論了各種可能的解決方法,包括:

基于 trait 的泛型

聲明宏

過(guò)程宏

外部代碼生成器

并且系統(tǒng)性地闡述了它們的關(guān)系和工程實(shí)現(xiàn)中的利弊:

ab70417a-b938-11ee-8b88-92fbcf53809c.png

從方法論的角度來(lái)講,一旦開(kāi)發(fā)者在某個(gè)需要使用泛型的地方使用了宏展開(kāi),調(diào)用它的代碼就不可能再通過(guò) trait-based generics 使用這段代碼。從這個(gè)角度來(lái)說(shuō),越是“大道至簡(jiǎn)”的生成代碼,越難維護(hù)。但反過(guò)來(lái)說(shuō),如果要完全實(shí)現(xiàn) trait-based generics,往往要和編譯器斗智斗勇,就算是通過(guò)編譯也需要花掉大量的時(shí)間。

我們首先來(lái)看基于 trait 泛型的解決方案。在 arrow-rs 中有一個(gè)名為 binary[2] 的 kernel 就是做這個(gè)的:給定一個(gè)二元標(biāo)量函數(shù),將其應(yīng)用于兩個(gè) array 進(jìn)行向量化計(jì)算,并生成一個(gè)新的 array。它的函數(shù)簽名如下:

pubfnbinary(
a:&PrimitiveArray,
b:&PrimitiveArray,
op:F
)->Result,ArrowError>
where
A:ArrowPrimitiveType,
B:ArrowPrimitiveType,
O:ArrowPrimitiveType,
F:Fn(::Native,::Native)->::Native,

相信你已經(jīng)開(kāi)始感受到「類(lèi)型體操」的味道了。盡管如此,它依然有以下這些局限:

支持的類(lèi)型僅限于 PrimitiveArray ,也就是 int, float, decimal 等基礎(chǔ)類(lèi)型。對(duì)于復(fù)雜類(lèi)型,如 bytes, string, list, struct,因?yàn)闆](méi)有統(tǒng)一到一個(gè) trait 下,所以每種都需要一個(gè)新的函數(shù)。

僅適用于兩個(gè)參數(shù)的函數(shù)。對(duì)于一個(gè)或更多參數(shù),每一種都需要這樣一個(gè)函數(shù)。arrow-rs 中也只內(nèi)置了 unary 和 binary 兩種 kernel。

僅適用于一種標(biāo)量函數(shù)簽名,即不出錯(cuò)的、不接受空值的函數(shù)??紤]其它各種可能的情況下,需要有不同的 F 定義:

fnadd(i32,i32)->i32;
fnchecked_add(i32,i32)->Result;
fnoptional_add(i32,Option)->Option;

如果考慮以上三種因素的結(jié)合,那么可能的組合無(wú)窮盡也,不可能覆蓋所有的函數(shù)類(lèi)型。

2.3 類(lèi)型體操 + 聲明宏

在文章《類(lèi)型體操》及 RisingWave 的初版實(shí)現(xiàn)中,作者使用 泛型 + 聲明宏 的方法部分解決了以上問(wèn)題:

1. 首先設(shè)計(jì)一套精妙的類(lèi)型系統(tǒng),將全部類(lèi)型統(tǒng)一到一個(gè) trait 下,解決了第一個(gè)問(wèn)題。 ab88452c-b938-11ee-8b88-92fbcf53809c.png

2. 然后,使用聲明宏來(lái)生成多種類(lèi)型的 kernel 函數(shù)。覆蓋常見(jiàn)的 1、2、3 個(gè)參數(shù),以及 T 和 Option 的輸入輸出組合。生成了常用的 unary binary ternary unary_nullable unary_bytes 等 kernel,部分解決了第二三個(gè)問(wèn)題。(具體實(shí)現(xiàn)參見(jiàn) RisingWave 早期代碼[3])當(dāng)然,這里理論上也可以繼續(xù)使用類(lèi)型體操。例如,引入 trait 統(tǒng)一 (A,) (A, B) (A, B, C) ,用 Into, AsRef trait 統(tǒng)一 T, Option, Result等。只不過(guò),大概率迎接我們的是 rustc 帶來(lái)的一點(diǎn)小小的類(lèi)型震撼:)

3. 最后,這些 kernel 沒(méi)有解決類(lèi)型動(dòng)態(tài) downcast 的問(wèn)題。為此,作者又利用聲明宏設(shè)計(jì)了一套精妙的宏套宏機(jī)制來(lái)實(shí)現(xiàn)動(dòng)態(tài)派發(fā)。

macro_rules!for_all_cmp_combinations{
($macro:tt$(,$x:tt)*)=>{
$macro!{
[$($x),*],
//comparisonacrossintegertypes
{int16,int32,int32},
{int32,int16,int32},
{int16,int64,int64},
//...

盡管解決了一些問(wèn)題,但這套方案依然有它的痛點(diǎn):

基于 trait 做類(lèi)型體操使我們不可避免地陷入到與 Rust 編譯器斗智斗勇之中。

依然沒(méi)有全面覆蓋所有可能情況。有相當(dāng)一部分函數(shù)仍然需要開(kāi)發(fā)者手寫(xiě)向量化實(shí)現(xiàn)。

性能。當(dāng)我們需要引入 SIMD 對(duì)部分函數(shù)進(jìn)行優(yōu)化時(shí),需要重新實(shí)現(xiàn)一套 kernel 函數(shù)。

沒(méi)有對(duì)開(kāi)發(fā)者隱藏全部細(xì)節(jié)。函數(shù)開(kāi)發(fā)者依然需要先熟悉類(lèi)型體操和聲明宏的工作原理,才能比較流暢地添加函數(shù)。

究其原因,我認(rèn)為是函數(shù)的變體形式過(guò)于復(fù)雜,而 Rust 的 trait 和聲明宏系統(tǒng)的靈活性不足導(dǎo)致的。本質(zhì)上是一種編程能力不夠強(qiáng)大的表現(xiàn)。

2.4 元編程?

讓我們來(lái)看看其他語(yǔ)言和框架是怎么解決這個(gè)問(wèn)題的。

首先是 Python,一種靈活的動(dòng)態(tài)類(lèi)型語(yǔ)言。這是 Flink 中的 Python UDF 接口,其它大數(shù)據(jù)系統(tǒng)的接口也大同小異:

@udf(result_type='BIGINT')
defadd(i,j):
returni+j

我們發(fā)現(xiàn)它是用 @udf 這個(gè)裝飾器標(biāo)記了函數(shù)的簽名信息,然后在運(yùn)行時(shí)對(duì)不同類(lèi)型進(jìn)行相應(yīng)的處理。當(dāng)然,由于它本身是動(dòng)態(tài)類(lèi)型,因此 Rust 中的很多問(wèn)題在 Python 中根本不存在,代價(jià)則是性能損失。

接下來(lái)是 Java,它是一種靜態(tài)類(lèi)型語(yǔ)言,但通過(guò)虛擬機(jī) JIT 運(yùn)行。這是 Flink 中的 Java UDF 接口:

publicstaticclassSubstringFunctionextendsScalarFunction{
publicStringeval(Strings,Integerbegin,Integerend){
returns.substring(begin,end);
}
}

可以看到同樣也很短。這次甚至不需要額外標(biāo)記類(lèi)型了,因?yàn)殪o態(tài)類(lèi)型系統(tǒng)本身就包含了類(lèi)型信息。我們可以通過(guò)運(yùn)行時(shí)反射拿到類(lèi)型信息,并通過(guò) JIT 機(jī)制在運(yùn)行時(shí)生成高效的強(qiáng)類(lèi)型代碼,兼具靈活與性能。

最后是 Zig,一種新時(shí)代的 C 語(yǔ)言。它最大的特色是任何代碼都可以加上 comptime 關(guān)鍵字在編譯時(shí)運(yùn)行,因此具備非常強(qiáng)的元編程能力。tygg 在博文「Zig lang 初體驗(yàn) -- 『大道至簡(jiǎn)』的 comptime[4]」中演示了用 Zig 實(shí)現(xiàn)遲先生類(lèi)型體操的方法:通過(guò) 編譯期反射 和 過(guò)程式的代碼生成 來(lái)代替開(kāi)發(fā)者完成類(lèi)型體操。

用一張表總結(jié)一下:

語(yǔ)言 類(lèi)型反射 代碼生成 靈活性 性能
Python 運(yùn)行時(shí)
Java 運(yùn)行時(shí) 運(yùn)行時(shí)
Zig 編譯時(shí) 編譯時(shí)
Rust (trait + macro_rules) 編譯時(shí)

可以發(fā)現(xiàn),Zig 語(yǔ)言強(qiáng)大的元編程能力提供了相對(duì)最好的解決方案。

2.5 過(guò)程宏

那么 Rust 里面有沒(méi)有類(lèi)似 Zig 的特性呢。其實(shí)是有的,那就是過(guò)程宏(Procedural Macros)。它可以在編譯期動(dòng)態(tài)執(zhí)行任何 Rust 代碼來(lái)修改 Rust 程序本身。只不過(guò),它的編譯時(shí)和運(yùn)行時(shí)代碼是物理分開(kāi)的,相比 Zig 的體驗(yàn)沒(méi)有那么統(tǒng)一,但是效果幾乎一樣。

參考 Python UDF 的接口設(shè)計(jì),我們便得到了 ”大道至簡(jiǎn)“ 的 Rust 函數(shù)接口:

#[function("add(int,int)->int")]
fnadd(a:i32,b:i32)->i32{
a+b
}

從用戶(hù)的角度看,他只需要在自己熟悉的 Rust 函數(shù)上面標(biāo)一個(gè)函數(shù)簽名。其它的類(lèi)型體操和代碼生成操作都被隱藏在過(guò)程宏之后,完全無(wú)需關(guān)心。

此時(shí)我們已經(jīng)拿到了一個(gè)函數(shù)所必須的全部信息,接下來(lái)我們將看到過(guò)程宏如何生成向量化執(zhí)行所需的樣板代碼。

3展開(kāi) #[function]

3.1 解析函數(shù)簽名

首先我們要實(shí)現(xiàn)類(lèi)型反射,也就是分別解析 SQL 函數(shù)和 Rust 函數(shù)的簽名,以此決定后面如何生成代碼。在過(guò)程宏入口處我們會(huì)拿到兩個(gè) TokenStream,分別包含了標(biāo)注信息和函數(shù)本體:

#[proc_macro_attribute]
pubfnfunction(attr:TokenStream,item:TokenStream)->TokenStream{
//attr:"add(int,int)->int"
//item:fnadd(a:i32,b:i32)->i32{a+b}
...
}

我們使用 syn 庫(kù)將 TokenStream 轉(zhuǎn)為 AST,然后:

解析 SQL 函數(shù)簽名字符串,獲取函數(shù)名、輸入輸出類(lèi)型等信息。

解析 Rust 函數(shù)簽名,獲取函數(shù)名、每個(gè)參數(shù)和返回值的類(lèi)型模式、是否 async 等信息。

具體地:

對(duì)于參數(shù)類(lèi)型,我們確定它是 T 或者 Option

對(duì)于返回值類(lèi)型,我們將其識(shí)別為:T,Option,Result ,Result> 四種類(lèi)型之一。

這將決定我們后面如何調(diào)用函數(shù)以及處理錯(cuò)誤。

3.2 定義類(lèi)型表

作為 trait 類(lèi)型體操的代替方案,我們?cè)谶^(guò)程宏中定義了這樣一張類(lèi)型表,來(lái)描述類(lèi)型系統(tǒng)之間的對(duì)應(yīng)關(guān)系,并且提供了相應(yīng)的查詢(xún)函數(shù)。

//nameprimitivearrayprefixdatatype
constTYPE_MATRIX:&str="
void_NullNull
boolean_BooleanBoolean
smallintyInt16Int16
intyInt32Int32
bigintyInt64Int64
realyFloat32Float32
floatyFloat64Float64
...
varchar_StringUtf8
bytea_BinaryBinary
array_ListList
struct_StructStruct
";

比如當(dāng)我們拿到用戶(hù)的函數(shù)簽名后,

#[function("length(varchar)->int")]

查表即可得知:

第一個(gè)參數(shù) varchar 對(duì)應(yīng)的 array 類(lèi)型為 StringArray

返回值 int 對(duì)應(yīng)的數(shù)據(jù)類(lèi)型為 DataType::Int32,對(duì)應(yīng)的 Builder 類(lèi)型為 Int32Builder

并非所有輸入輸出均為 primitive 類(lèi)型,因此無(wú)法進(jìn)行 SIMD 優(yōu)化

在下面的代碼生成中,這些類(lèi)型將被填入到對(duì)應(yīng)的位置。

3.3 生成求值代碼

在代碼生成階段,我們主要使用 quote 庫(kù)來(lái)生成并組合代碼片段。最終生成的代碼整體結(jié)構(gòu)如下:

quote!{
struct#struct_name;
implScalarFunctionfor#struct_name{
fneval(&self,input:&RecordBatch)->Result{
#downcast_arrays
letmutbuilder=#builder;
#eval
Ok(Arc::new(builder.finish()))
}
}
}

下面我們來(lái)逐個(gè)填寫(xiě)代碼片段,首先是 downcast 輸入 array:

letchildren_indices=(0..self.args.len());
letarrays=children_indices.map(|i|format_ident!("a{i}"));
letarg_arrays=children_indices.map(|i|format_ident!("{}",types::array_type(&self.args[*i])));

letdowncast_arrays=quote!{
#(
let#arrays:&#arg_arrays=input.column(#children_indices).as_any().downcast_ref()
.ok_or_else(||ArrowError::CastError(...))?;
)*
};

builder:

letbuilder_type=format_ident!("{}",types::array_builder_type(ty));
letbuilder=quote!{#builder_type::with_capacity(input.num_rows())};

接下來(lái)是最關(guān)鍵的執(zhí)行部分,我們先寫(xiě)出函數(shù)調(diào)用的那一行:

letinputs=children_indices.map(|i|format_ident!("i{i}"));
letoutput=quote!{#user_fn_name(#(#inputs,)*)};
//example:add(i0,i1)

然后考慮:這個(gè)表達(dá)式返回了什么類(lèi)型呢?這需要根據(jù) Rust 函數(shù)簽名決定,它可能包含 Option,也可能包含 Result。我們進(jìn)行錯(cuò)誤處理,然后將其歸一化到 Option 類(lèi)型:

letoutput=matchuser_fn.return_type_kind{
T=>quote!{Some(#output)},
Option=>quote!{#output},
Result=>quote!{Some(#output?)},
ResultOption=>quote!{#output?},
};
//example:Some(add(i0,i1))

下面考慮:這個(gè)函數(shù)接收什么樣的類(lèi)型作為輸入?這同樣需要根據(jù) Rust 函數(shù)簽名決定,每個(gè)參數(shù)可能是或不是 Option。如果函數(shù)不接受 Option 輸入,但實(shí)際輸入的卻是 null,那么我們默認(rèn)它的返回值就是 null,此時(shí)無(wú)需調(diào)用函數(shù)。因此,我們使用 match 語(yǔ)句來(lái)對(duì)輸入?yún)?shù)做預(yù)處理:

letsome_inputs=inputs.iter()
.zip(user_fn.arg_is_option.iter())
.map(|(input,opt)|{
if*opt{
quote!{#input}
}else{
quote!{Some(#input)}
}
});
letoutput=quote!{
//這里的inputs是從array中拿出來(lái)的Option
match(#(#inputs,)*){
//我們將部分參數(shù)unwrap后再喂給函數(shù)
(#(#some_inputs,)*)=>#output,
//如有unwrap失敗則直接返回null
_=>None,
}
};
//example:
//match(i0,i1){
//(Some(i0),Some(i1))=>Some(add(i0,i1)),
//_=>None,
//}

此時(shí)我們已經(jīng)拿到了一行的返回值,可以將它 append 到 builder 中:

letappend_output=quote!{builder.append_option(#output);};

最后在外面套一層循環(huán),對(duì)輸入逐行操作:

leteval=quote!{
for(i,(#(#inputs,)*))inmultizip((#(#arrays.iter(),)*)).enumerate(){
#append_output
}
};

如果一切順利的話(huà),過(guò)程宏展開(kāi)生成的代碼將如 2.1 節(jié)中所示的那樣。

3.4 函數(shù)注冊(cè)

到此為止我們已經(jīng)完成了最核心、最困難的部分,即生成向量化求值代碼。但是,用戶(hù)該怎么使用生成的代碼呢?

注意到一開(kāi)始我們生成了一個(gè) struct。因此,我們可以允許用戶(hù)指定這個(gè) struct 的名稱(chēng),或者定義一套規(guī)范自動(dòng)生成唯一的名稱(chēng)。這樣用戶(hù)就能在這個(gè) struct 上調(diào)用函數(shù)了。

//指定生成名為Add的struct
#[function("add(int,int)->int",output="Add")]
fnadd(a:i32,b:i32)->i32{
a+b
}

//調(diào)用生成的向量化求值函數(shù)
letinput:RecordBatch=...;
letoutput:RecordBatch=Add.eval(&input).unwrap();

不過(guò)在實(shí)際場(chǎng)景中,很少有這種使用特定函數(shù)的需求。更多是在項(xiàng)目中定義很多函數(shù),然后在解析 SQL 查詢(xún)時(shí),動(dòng)態(tài)地查找匹配的函數(shù)。為此我們需要一種全局的函數(shù)注冊(cè)和查找機(jī)制。

問(wèn)題來(lái)了:Rust 本身沒(méi)有反射機(jī)制,如何在運(yùn)行時(shí)獲取所有由 #[function] 靜態(tài)定義的函數(shù)呢?

答案是:利用程序的鏈接時(shí)(link time)特性,將函數(shù)指針等元信息放入特定的 section 中。程序鏈接時(shí),鏈接器(linker)會(huì)自動(dòng)收集分布在各處的符號(hào)(symbol)集中在一起。程序運(yùn)行時(shí)即可掃描這個(gè) section 獲取全部函數(shù)了。

Rust 社區(qū)的 dtolnay 大佬為此需求做了兩個(gè)開(kāi)箱即用的庫(kù):linkme[5] 和 inventory[6]。其中前者是直接利用上述機(jī)制,后者是利用 C 標(biāo)準(zhǔn)的 constructor 初始化函數(shù),但背后的原理沒(méi)有本質(zhì)區(qū)別。下面我們以 linkme 為例來(lái)演示如何實(shí)現(xiàn)注冊(cè)機(jī)制。

首先我們需要在公共庫(kù)(而不是 proc-macro)中定義函數(shù)簽名的結(jié)構(gòu):

pubstructFunctionSignature{
pubname:String,
pubarg_types:Vec,
pubreturn_type:DataType,
pubfunction:Box,
}

然后定義一個(gè)全局變量 REGISTRY 作為注冊(cè)中心。它會(huì)在第一次被訪(fǎng)問(wèn)時(shí)利用 linkme 將所有 #[function] 定義的函數(shù)收集到一個(gè) HashMap 中:

///Acollectionofdistributed`#[function]`signatures.
#[linkme::distributed_slice]
pubstaticSIGNATURES:[fn()->FunctionSignature];

lazy_static::lazy_static!{
///Globalfunctionregistry.
pubstaticrefREGISTRY:FunctionRegistry={
letmutsignatures=HashMap::>::new();
forsiginSIGNATURES{
letsig=sig();
signatures.entry(sig.name.clone()).or_default().push(sig);
}
FunctionRegistry{signatures}
};
}

最后在 #[function] 過(guò)程宏中,我們?yōu)槊總€(gè)函數(shù)生成如下代碼:

#[linkme::distributed_slice(SIGNATURES)]
fn#sig_name()->FunctionSignature{
FunctionSignature{
name:#name.into(),
arg_types:vec![#(#args),*],
return_type:#ret,
//這里#struct_name就是我們之前生成的函數(shù)結(jié)構(gòu)體
function:Box::new(#struct_name),
}
}

如此一來(lái),用戶(hù)就可以通過(guò) FunctionRegistry 提供的方法動(dòng)態(tài)查找函數(shù)并進(jìn)行求值了:

letgcd=REGISTRY.get("gcd",&[Int32,Int32],&Int32);
letoutput:RecordBatch=gcd.function.eval(&input).unwrap();

3.5 小結(jié)

以上我們完整闡述了 #[function] 過(guò)程宏的工作原理和實(shí)現(xiàn)過(guò)程:

使用 syn 庫(kù)解析函數(shù)簽名

使用 quote 庫(kù)生成定制化的向量化求值代碼

使用 linkme 庫(kù)實(shí)現(xiàn)函數(shù)的全局注冊(cè)和動(dòng)態(tài)查找

其中:

SQL 簽名決定了如何從 input array 中讀取數(shù)據(jù),如何生成 output array

Rust 簽名決定了如何調(diào)用用戶(hù)的 Rust 函數(shù),如何處理空值和錯(cuò)誤

類(lèi)型查找表決定了 SQL 類(lèi)型和 Rust 類(lèi)型的映射關(guān)系

相比 trait + 聲明宏的解決方案,過(guò)程宏中的 “過(guò)程式” 風(fēng)格為我們提供了極大的靈活性,一攬子解決了之前提到的全部問(wèn)題。在下一章中,我們將會(huì)在這個(gè)框架的基礎(chǔ)上繼續(xù)擴(kuò)展,解決更多實(shí)際場(chǎng)景下的復(fù)雜需求。

4高級(jí)功能

抽象的問(wèn)題是簡(jiǎn)單的,但現(xiàn)實(shí)的需求是復(fù)雜的。上面的原型看似解決了所有問(wèn)題,但在 RisingWave 的實(shí)際工程開(kāi)發(fā)中,我們遇到了各種稀奇古怪的需求,都無(wú)法用最原始的 #[function] 宏實(shí)現(xiàn)。下面我們來(lái)逐一介紹這些問(wèn)題,并利用過(guò)程宏的靈活性見(jiàn)招拆招。

4.1 支持多類(lèi)型重載

有些函數(shù)支持大量不同類(lèi)型的重載,例如 + 運(yùn)算對(duì)幾乎支持所有數(shù)字類(lèi)型。此時(shí)我們一般會(huì)復(fù)用同一個(gè)泛型函數(shù),然后用不同的類(lèi)型去實(shí)例化它。

#[function("add(*int,*int)->auto")]
#[function("add(*float,*float)->auto")]
#[function("add(decimal,decimal)->decimal")]
#[function("add(interval,interval)->interval")]
fnadd(l:T1,r:T2)->Result
where
T1:Into+Debug,
T2:Into+Debug,
T3:CheckedAdd,
{
a.into().checked_add(b.into()).ok_or(ExprError::NumericOutOfRange)
}

因此我們支持在同一個(gè)函數(shù)上同時(shí)標(biāo)記多個(gè)#[function] 宏。此外,我們還支持使用類(lèi)型通配符將一個(gè)#[function] 自動(dòng)展開(kāi)成多個(gè),并使用 auto 自動(dòng)推斷返回類(lèi)型。例如 *int 通配符表示全部整數(shù)類(lèi)型 int2, int4, int8,那么 add(*int, *int) 將展開(kāi)為 3 x 3 = 9 種整數(shù)的組合,返回值自動(dòng)推斷為兩種類(lèi)型中最大的一個(gè):

#[function("add(int2,int2)->int2")]
#[function("add(int2,int4)->int4")]
#[function("add(int2,int8)->int8")]
#[function("add(int4,int4)->int4")]
...

而如果泛型不能滿(mǎn)足一些特殊類(lèi)型的要求,你也完全可以定義新函數(shù)進(jìn)行特化(specialization):

#[function("add(interval,timestamp)->timestamp")]
fninterval_timestamp_add(l:Interval,r:Timestamp)->Result{
r.checked_add(l).ok_or(ExprError::NumericOutOfRange)
}

這一特性幫助我們快速實(shí)現(xiàn)函數(shù)重載,同時(shí)避免了冗余代碼。

4.2 自動(dòng) SIMD 優(yōu)化

作為零開(kāi)銷(xiāo)抽象語(yǔ)言,Rust 從不向性能妥協(xié),#[function] 宏也是如此。對(duì)于很多簡(jiǎn)單函數(shù),理論上可以利用 CPU 內(nèi)置的 SIMD 指令實(shí)現(xiàn)上百倍的性能提升。然而,編譯器往往只能對(duì)簡(jiǎn)單的循環(huán)結(jié)構(gòu)實(shí)現(xiàn)自動(dòng) SIMD 向量化。一旦循環(huán)中出現(xiàn)分支跳轉(zhuǎn)等復(fù)雜結(jié)構(gòu),自動(dòng)向量化就會(huì)失效。

//簡(jiǎn)單循環(huán)支持自動(dòng)向量化
assert_eq!(a.len(),n);
assert_eq!(b.len(),n);
assert_eq!(c.len(),n);
foriin0..n{
c[i]=a[i]+b[i];
}

//一旦出現(xiàn)分支結(jié)構(gòu),如錯(cuò)誤處理、越界檢查等,自動(dòng)向量化就會(huì)失效
foriin0..n{
c.push(a[i].checked_add(b[i])?);
}

不幸的是,我們前文中生成的代碼結(jié)構(gòu)并不利于編譯器進(jìn)行自動(dòng)向量化,因?yàn)檠h(huán)中的 builder.append_option() 操作本身就自帶條件分支。

為了支持自動(dòng)向量化,我們需要對(duì)代碼生成邏輯進(jìn)一步特化:

首先根據(jù)函數(shù)簽名判斷這個(gè)函數(shù)能否實(shí)現(xiàn) SIMD 優(yōu)化。這需要滿(mǎn)足以下兩個(gè)主要條件:

比如:

#[function("equal(int,int)->boolean")]
fnequal(a:i32,b:i32)->bool{
a==b
}

所有輸入輸出類(lèi)型均為基礎(chǔ)類(lèi)型,即 boolean, int, float, decimal

Rust 函數(shù)的輸入類(lèi)型均不含 Option,輸出不含 Option 和 Result

一旦上述條件滿(mǎn)足,我們會(huì)對(duì) #eval 代碼段進(jìn)行特化,將其替換為這樣的代碼,調(diào)用 arrow-rs 內(nèi)置的 unary 和 binary kernel 實(shí)現(xiàn)自動(dòng)向量化:

//SIMDoptimizationforprimitivetypes
matchself.args.len(){
0=>quote!{
letc=#ret_array_type::from_iter_values(
std::repeat_with(||#user_fn_name()).take(input.num_rows())
);
letarray=Arc::new(c);
},
1=>quote!{
letc:#ret_array_type=arrow_arith::unary(a0,#user_fn_name);
letarray=Arc::new(c);
},
2=>quote!{
letc:#ret_array_type=arrow_arith::binary(a0,a1,#user_fn_name)?;
letarray=Arc::new(c);
},
n=>todo!("SIMDoptimizationfor{n}arguments"),
}

需要注意,如果用戶(hù)函數(shù)本身包含分支結(jié)構(gòu),那么自動(dòng)向量化也是無(wú)效的。我們只是盡力為編譯器創(chuàng)造了實(shí)現(xiàn)優(yōu)化的條件。另一方面,這一優(yōu)化也不是完全安全的,它會(huì)使得原本為 null 的輸入強(qiáng)制執(zhí)行。例如整數(shù)除法 a / b,如果 b 為 null,原本不會(huì)執(zhí)行,現(xiàn)在卻會(huì)執(zhí)行 a / 0,導(dǎo)致除零異常而崩潰。這種情況下我們只能修改函數(shù)簽名,避免生成特化代碼。

整體而言,實(shí)現(xiàn)這一功能后,用戶(hù)編寫(xiě)代碼不需要有任何變化,但是部分函數(shù)的性能得到了大幅提高。這對(duì)于高性能數(shù)據(jù)處理系統(tǒng)而言是必須的。

4.3 返回字符串直接寫(xiě)入 buffer

很多函數(shù)會(huì)返回字符串。但是樸素地返回 String 會(huì)導(dǎo)致大量動(dòng)態(tài)內(nèi)存分配,降低性能。

#[function("concat(varchar,varchar)->varchar")]
fnconcat(left:&str,right:&str)->String{
format!("{left}{right}")
}

注意到列式內(nèi)存存儲(chǔ)中,StringArray 實(shí)際上是把多個(gè)字符串存放在一段連續(xù)的內(nèi)存上,構(gòu)建這個(gè)數(shù)組的 StringBuilder 實(shí)際上也只是將字符串追加寫(xiě)入同一個(gè) buffer 里。因此函數(shù)返回 String 是沒(méi)有必要的,它可以直接將字符串寫(xiě)入 StringBuilder 的 buffer 中。

于是我們支持對(duì)返回字符串的函數(shù)添加一個(gè) &mut Write 類(lèi)型的 writer 參數(shù)。內(nèi)部可以直接用 write! 方法向 writer 寫(xiě)入返回值。

#[function("concat(varchar,varchar)->varchar")]
fnconcat(left:&str,right:&str,writer:&mutimplstd::Write){
writer.write_str(left).unwrap();
writer.write_str(right).unwrap();
}

在過(guò)程宏的實(shí)現(xiàn)中,我們主要修改了函數(shù)調(diào)用部分:

letwriter=user_fn.write.then(||quote!{&mutbuilder,});
letoutput=quote!{#user_fn_name(#(#inputs,)*#writer)};

以及特化 append_output 的邏輯:

letappend_output=ifuser_fn.write{
quote!{{
if#output.is_some(){//返回值直接在這行寫(xiě)入builder
builder.append_value("");
}else{
builder.append_null();
}
}}
}else{
quote!{builder.append_option(#output);}
};

經(jīng)過(guò)測(cè)試,這一功能也可以大幅提升字符串處理函數(shù)的性能。

4.4 常量預(yù)處理優(yōu)化

有些函數(shù)的某個(gè)參數(shù)往往是一個(gè)常量,并且這個(gè)常量需要經(jīng)過(guò)一個(gè)開(kāi)銷(xiāo)較大的預(yù)處理過(guò)程。這類(lèi)函數(shù)的典型代表是正則表達(dá)式匹配:

//regexp_like(source,pattern)
#[function("regexp_like(varchar,varchar)->boolean")]
fnregexp_like(text:&str,pattern:&str)->Result{
letregex=regex::new(pattern)?;//預(yù)處理:編譯正則表達(dá)式
Ok(regex.is_match(text))
}

對(duì)于一次向量化求值來(lái)說(shuō),如果輸入的 pattern 是常數(shù)(very likely),那么其實(shí)只需要編譯一次,然后用編譯后的數(shù)據(jù)結(jié)構(gòu)對(duì)每一行文本進(jìn)行匹配即可。但如果不是常數(shù)(unlikely,但是合法行為),則需要對(duì)每一行 pattern 編譯一次再執(zhí)行。

為了支持這一需求,我們修改用戶(hù)接口,將特定參數(shù)的預(yù)處理過(guò)程提取到過(guò)程宏中,然后把預(yù)處理后的類(lèi)型作為參數(shù):

#[function(
"regexp_like(varchar,varchar)->boolean",
prebuild="Regex::new($1)?"http://$1表示第一個(gè)參數(shù)(下標(biāo)從0開(kāi)始)
)]
fnregexp_like(text:&str,regex:&Regex)->bool{
regex.is_match(text)
}

這樣,過(guò)程宏可以對(duì)這個(gè)函數(shù)生成兩個(gè)版本的代碼:

如果指定參數(shù)為常量,那么在構(gòu)造函數(shù)中執(zhí)行 prebuild 代碼,并將生成的 Regex 中間值存放在 struct 當(dāng)中,在求值階段直接傳入函數(shù)。

如果不是常量,那么在求值階段將 prebuild 代碼嵌入到函數(shù)參數(shù)的位置上。

至于具體的代碼生成邏輯,由于細(xì)節(jié)相當(dāng)復(fù)雜,這里就不再展開(kāi)介紹了。

總之,這一優(yōu)化保證了此類(lèi)函數(shù)各種輸入下都具有最優(yōu)性能,并且極大簡(jiǎn)化了手工實(shí)現(xiàn)的復(fù)雜性。

4.5 表函數(shù)

最后,我們來(lái)看表函數(shù)(Table Function,Postgres 中也稱(chēng) Set-returning Funcion,返回集合的函數(shù))。這類(lèi)函數(shù)的返回值不再是一行,而是多行。如果同時(shí)返回多列,那么就相當(dāng)于返回一個(gè)表。

select*fromgenerate_series(1,3);
generate_series
-----------------
1
2
3

對(duì)應(yīng)到常見(jiàn)的編程語(yǔ)言中,實(shí)際是一個(gè)生成器函數(shù)(Generator)。以 Python 為例,可以寫(xiě)成這樣:

defgenerate_series(start,end):
foriinrange(start,end+1):
yieldi

Rust 語(yǔ)言目前在 nightly 版本支持生成器,但這一特性尚未 stable。不過(guò)如果不用 yield 語(yǔ)法的話(huà),我們可以利用 RPIT 特性實(shí)現(xiàn)返回迭代器的函數(shù),以達(dá)到同樣的效果:

#[function("generate_series(int,int)->setofint")]
fngenerate_series(start:i32,stop:i32)->implIterator{
start..=stop
}

我們支持在 #[function] 簽名中使用 -> setof 以聲明一個(gè)表函數(shù)。它修飾的 Rust 函數(shù)必須返回一個(gè) impl Iterator,其中的 Item 需要匹配返回類(lèi)型。當(dāng)然,Iterator 的內(nèi)外都可以包含 Option 或 Result。

在對(duì)表函數(shù)進(jìn)行向量化求值時(shí),我們會(huì)對(duì)每一行輸入調(diào)用生成器函數(shù),然后將每一行返回的多行結(jié)果串聯(lián)起來(lái),最后按照固定的 chunk size 進(jìn)行切割,依次返回多個(gè) RecordBatch。因此表函數(shù)的向量化接口長(zhǎng)這個(gè)樣子:

pubtraitTableFunction{
fneval(&self,input:&RecordBatch,chunk_size:usize)
->Result>>>;
}

我們給出一組 generate_series 的輸入輸出樣例(假設(shè) chunk size = 2):

inputoutput
+-------+------++-----+-----------------+
|start|stop||row|generate_series|
+-------+------++-----+-----------------+
|0|0|---->|0|0|
|||+->|2|0|
|0|2|--++-----+-----------------+
+-------+------+|2|1|
|2|2|
+-----+-----------------+

由于表函數(shù)的輸入輸出不再具有一對(duì)一的關(guān)系,我們?cè)?output 中會(huì)額外生成一列row來(lái)表示每一行輸出對(duì)應(yīng) input 中的哪一行輸入。這一關(guān)系信息會(huì)在某些 SQL 查詢(xún)中被使用到。

回到#[function]宏的實(shí)現(xiàn),它為表函數(shù)生成的代碼實(shí)際上也是一個(gè)生成器。我們?cè)趦?nèi)部使用了futures_async_stream[7]提供的#[try_stream]宏實(shí)現(xiàn) async generator(它依賴(lài) nightly 的 generator 特性),在 stable 版本中則使用genawaiter[8]代替。之所以要使用生成器,則是因?yàn)橐粋€(gè)表函數(shù)可能會(huì)生成非常長(zhǎng)的結(jié)果(例如generate_series(0, 1000000000)),中途必須把控制權(quán)交還調(diào)用者,才能保證系統(tǒng)不被卡死。感興趣的讀者可以思考一下:如果沒(méi)有 generator 機(jī)制,高效的向量化表函數(shù)求值能否實(shí)現(xiàn)?如何實(shí)現(xiàn)?

說(shuō)到這里,多扯兩句。genawaiter 也是個(gè)很有意思的庫(kù),它使用 async-await 機(jī)制來(lái)在 stable Rust 中實(shí)現(xiàn) generator。我們知道 async-await 本質(zhì)上也是一種 generator,它們都依賴(lài)編譯器的 CPS 變換實(shí)現(xiàn)狀態(tài)機(jī)。不過(guò)出于對(duì)異步編程的強(qiáng)烈需求,async-await 很早就被穩(wěn)定化,而 generator 卻遲遲沒(méi)有穩(wěn)定。由于背后的原理相通,它們可以互相實(shí)現(xiàn)。 此外,目前 Rust 社區(qū)正在積極推動(dòng) async generator 的進(jìn)展,原生的async gen[9]和for await[10]語(yǔ)法剛剛在上個(gè)月進(jìn)入 nightly。不過(guò)由于沒(méi)有和 futures 生態(tài)對(duì)接,整體依然處于不可用狀態(tài)。RisingWave 的流處理引擎就深度依賴(lài) async generator 機(jī)制實(shí)現(xiàn)流算子,以簡(jiǎn)化異步 IO 下的流狀態(tài)管理。不過(guò)這又是一個(gè)龐大的話(huà)題,之后有機(jī)會(huì)再來(lái)介紹這方面的應(yīng)用吧。

5總結(jié)

由于篇幅所限,我們只能展開(kāi)這么多了。如你所見(jiàn),一個(gè)簡(jiǎn)單的函數(shù)求值背后,隱藏著非常多的設(shè)計(jì)和實(shí)現(xiàn)細(xì)節(jié):

為了高性能,我們選擇列式內(nèi)存存儲(chǔ)和向量化求值。

存儲(chǔ)數(shù)據(jù)的容器通常是類(lèi)型擦除的結(jié)構(gòu)。但 Rust 是一門(mén)靜態(tài)類(lèi)型語(yǔ)言,用戶(hù)定義的函數(shù)是強(qiáng)類(lèi)型的簽名。這意味著我們需要在編譯期確定每一個(gè)容器的具體類(lèi)型,做類(lèi)型體操來(lái)處理不同類(lèi)型之間的轉(zhuǎn)換,準(zhǔn)確地把數(shù)據(jù)從容器中取出來(lái)喂給函數(shù),最后高效地將函數(shù)吐出來(lái)的結(jié)果打包回?cái)?shù)據(jù)容器中。

為了將上述過(guò)程隱藏起來(lái),我們?cè)O(shè)計(jì)了#[function]過(guò)程宏在編譯期做類(lèi)型反射和代碼生成,最終暴露給用戶(hù)一個(gè)盡可能簡(jiǎn)單直觀(guān)的接口。

但是實(shí)際工程中存在各種復(fù)雜需求以及對(duì)性能的要求,我們必須持續(xù)在接口上打洞,并對(duì)代碼生成邏輯進(jìn)行特化。幸好,過(guò)程宏具有非常強(qiáng)的靈活性,使得我們可以敏捷地應(yīng)對(duì)變化的需求。

#[function]宏最初是為 RisingWave 內(nèi)部函數(shù)實(shí)現(xiàn)的一套框架。最近,我們將它從 RisingWave 項(xiàng)目中獨(dú)立出來(lái),基于 Apache Arrow 標(biāo)準(zhǔn)化成一套通用的用戶(hù)定義函數(shù)接口arrow-udf[11]。如果你的項(xiàng)目也在使用 arrow-rs 進(jìn)行數(shù)據(jù)處理,現(xiàn)在可以直接使用這套#[function]宏定義自己的函數(shù)。如果你在使用 RisingWave,那么從這個(gè)月底發(fā)布的 1.7 版本起,你可以使用這個(gè)庫(kù)來(lái)定義 Rust UDF。它可以編譯成 WebAssembly 模塊插入到 RisingWave 中運(yùn)行。感興趣的讀者也可以閱讀這個(gè)項(xiàng)目的源碼了解更多實(shí)現(xiàn)細(xì)節(jié)。

事實(shí)上,RisingWave 基于 Apache Arrow 構(gòu)建了一整套用戶(hù)定義函數(shù)接口。此前,我們已經(jīng)實(shí)現(xiàn)了服務(wù)器模式的 Python 和 Java UDF。最近,我們又基于 WebAssembly 實(shí)現(xiàn)了 Rust UDF,基于 QuickJS 實(shí)現(xiàn)了 JavaScript UDF。它們都可以嵌入到 RisingWave 中運(yùn)行,以實(shí)現(xiàn)更好的性能和用戶(hù)體驗(yàn)。





審核編輯:劉清

聲明:本文內(nèi)容及配圖由入駐作者撰寫(xiě)或者入駐合作網(wǎng)站授權(quán)轉(zhuǎn)載。文章觀(guān)點(diǎn)僅代表作者本人,不代表電子發(fā)燒友網(wǎng)立場(chǎng)。文章及其配圖僅供工程師學(xué)習(xí)之用,如有內(nèi)容侵權(quán)或者其他違規(guī)問(wèn)題,請(qǐng)聯(lián)系本站處理。 舉報(bào)投訴
  • SQL
    SQL
    +關(guān)注

    關(guān)注

    1

    文章

    770

    瀏覽量

    44189
  • 生成器
    +關(guān)注

    關(guān)注

    7

    文章

    317

    瀏覽量

    21061
  • Rust
    +關(guān)注

    關(guān)注

    1

    文章

    229

    瀏覽量

    6626
  • ChatGPT
    +關(guān)注

    關(guān)注

    29

    文章

    1564

    瀏覽量

    7861

原文標(biāo)題:用 Rust 過(guò)程宏魔法簡(jiǎn)化 SQL 函數(shù)實(shí)現(xiàn)

文章出處:【微信號(hào):Rust語(yǔ)言中文社區(qū),微信公眾號(hào):Rust語(yǔ)言中文社區(qū)】歡迎添加關(guān)注!文章轉(zhuǎn)載請(qǐng)注明出處。

收藏 人收藏

    評(píng)論

    相關(guān)推薦

    淺談函數(shù)妙用!

    函數(shù)在項(xiàng)目開(kāi)發(fā)中用的頻率非常高,跟普通函數(shù)相比,它沒(méi)有復(fù)雜的調(diào)用步驟,也不需要給形參分配空間,所以很多場(chǎng)景都需要函數(shù)的存在。
    發(fā)表于 02-01 09:50 ?681次閱讀

    請(qǐng)教如何用SQL語(yǔ)句來(lái)壓縮ACCESS數(shù)據(jù)庫(kù)

    通過(guò)對(duì)ACCESS數(shù)據(jù)庫(kù)的“修復(fù)與壓縮”會(huì)使程序的運(yùn)行更加穩(wěn)定和提高運(yùn)行速度?!?qǐng)教如何用SQL語(yǔ)句來(lái)壓縮ACCESS數(shù)據(jù)庫(kù),只用SQL語(yǔ)句喲!謝謝!
    發(fā)表于 11-29 21:54

    何用Matlab去實(shí)現(xiàn)FFT函數(shù)和IFFT函數(shù)

    Matlab的FFT函數(shù)和IFFT函數(shù)有什么用法嗎?如何用Matlab去實(shí)現(xiàn)FFT函數(shù)和IFFT函數(shù)
    發(fā)表于 11-18 07:05

    何用 rust 語(yǔ)言開(kāi)發(fā) stm32

    本文介紹如何用 rust 語(yǔ)言開(kāi)發(fā) stm32。開(kāi)發(fā)平臺(tái)為 linux(gentoo)。硬件準(zhǔn)備本文使用的芯片為 STM32F103C8T6。該芯片性?xún)r(jià)比較高,價(jià)格低廉,適合入門(mén)學(xué)習(xí)。需要
    發(fā)表于 11-26 06:20

    何用__write函數(shù)替換掉原先的fputc函數(shù)

    何用__write函數(shù)替換掉原先的fputc函數(shù)?
    發(fā)表于 12-01 06:55

    如何對(duì)gcc編譯過(guò)程中生成的進(jìn)行調(diào)試

    如何對(duì)gcc編譯過(guò)程中生成的進(jìn)行調(diào)試?有哪幾種形式?如何對(duì)一個(gè)函數(shù)進(jìn)行g(shù)prof方式的剖析?
    發(fā)表于 12-24 07:53

    何用proc sql生成變量?

    上節(jié)我們講了PROC SQL的基本結(jié)構(gòu),以及一些sql命令的使用,這節(jié)我們主要講一下case...when...、order by 、group by 、update、delete語(yǔ)句以及如何用proc
    的頭像 發(fā)表于 05-19 16:13 ?2443次閱讀
    如<b class='flag-5'>何用</b>proc <b class='flag-5'>sql</b>生成<b class='flag-5'>宏</b>變量?

    C語(yǔ)言函數(shù)封裝技巧分享

    函數(shù),即包含多條語(yǔ)句的定義,其通常為某一被頻繁調(diào)用的功能的語(yǔ)句封裝,且不想通過(guò)函數(shù)方式封裝來(lái)降低額外的彈棧壓棧開(kāi)銷(xiāo)。
    的頭像 發(fā)表于 09-14 09:31 ?671次閱讀

    C語(yǔ)言函數(shù)怎樣實(shí)現(xiàn)封裝?

    函數(shù),即包含多條語(yǔ)句的定義,其通常為某一被頻繁調(diào)用的功能的語(yǔ)句封裝,且不想通過(guò)函數(shù)方式封裝來(lái)降低額外的彈棧壓棧開(kāi)銷(xiāo)。
    的頭像 發(fā)表于 09-22 09:23 ?778次閱讀

    C語(yǔ)言中函數(shù)的定義和用法

    函數(shù)是一種特殊的函數(shù),與普通函數(shù)不同的是,它可以擁有多條語(yǔ)句和局部變量,從而實(shí)現(xiàn)更復(fù)雜的功
    發(fā)表于 10-11 11:32 ?3653次閱讀
    C語(yǔ)言中<b class='flag-5'>宏</b><b class='flag-5'>函數(shù)</b>的定義和用法

    何用Rust通過(guò)JNI和Java進(jìn)行交互

    近期工作中有Rust和Java互相調(diào)用需求,這篇文章主要介紹如何用Rust通過(guò)JNI和Java進(jìn)行交互,還有記錄一下開(kāi)發(fā)過(guò)程中遇到的一些坑。
    的頭像 發(fā)表于 10-17 11:41 ?805次閱讀

    的缺陷與內(nèi)聯(lián)函數(shù)的引入

    雖然有著一定的優(yōu)勢(shì),但是它的缺點(diǎn)也不可忽視。 在編譯階段,我們很難發(fā)現(xiàn)代碼哪里出問(wèn)題了,因?yàn)?b class='flag-5'>宏替換是發(fā)生在預(yù)處理階段,所以有時(shí)候在函數(shù)傳參的時(shí)候發(fā)生一些錯(cuò)誤,編譯器不會(huì)發(fā)現(xiàn),那它調(diào)
    的頭像 發(fā)表于 11-01 17:57 ?468次閱讀

    sql中日期函數(shù)的用法

    日期函數(shù)SQL中是非常重要的功能之一,它們能幫助我們?cè)跀?shù)據(jù)庫(kù)中存儲(chǔ)和處理日期和時(shí)間數(shù)據(jù)。在本文中,我將詳細(xì)介紹一些常用的SQL日期函數(shù),包括如何創(chuàng)建日期和時(shí)間數(shù)據(jù)、如何格式化和轉(zhuǎn)換日
    的頭像 發(fā)表于 11-17 16:24 ?1100次閱讀

    如何利用Rust過(guò)程實(shí)現(xiàn)derive-with庫(kù)

    通過(guò)派生 #[derive(With)] 給結(jié)構(gòu)體字段生成 with_xxx 方法,通過(guò)鏈?zhǔn)秸{(diào)用 with_xxx 方法來(lái)構(gòu)造結(jié)構(gòu)體。
    的頭像 發(fā)表于 01-25 09:51 ?318次閱讀

    常用SQL函數(shù)及其用法

    SQL(Structured Query Language)是一種用于管理和操作關(guān)系數(shù)據(jù)庫(kù)的編程語(yǔ)言。SQL 提供了豐富的函數(shù)庫(kù),用于數(shù)據(jù)檢索、數(shù)據(jù)更新、數(shù)據(jù)刪除以及數(shù)據(jù)聚合等操作。以下是一些常用
    的頭像 發(fā)表于 11-19 10:18 ?361次閱讀