SQL 宏
SQL 宏注册的是 SQL 而不是 Rust 代码。可接受的返回形态有四种,但要把宏维护得省心,最省事的做法是把 SQL
写进 .sql 文件,然后注册这些文件。
从 .sql 文件注册宏
duck_sql_macro_files! 接受任意多个路径,编译期把每个文件内联,并按顺序执行:
duck_sql_macro_files!(
"sql/macro_files_a.sql",
"sql/macro_files_b.sql",
"sql/macro_files_c.sql"
);
sql/macro_files_a.sql:
CREATE OR REPLACE MACRO dfn_macro_files_add(a, b) AS (a + b);
CREATE OR REPLACE MACRO dfn_macro_files_mul(a, b) AS (a * b);
sql/macro_files_b.sql:
CREATE OR REPLACE MACRO dfn_macro_files_gen(n) AS TABLE SELECT * FROM range(n);
sql/macro_files_c.sql:
CREATE OR REPLACE MACRO dfn_macro_files_negate(x) AS (-x);
SELECT dfn_macro_files_add(2, 3); -- 5
SELECT dfn_macro_files_mul(4, 5); -- 20
SELECT * FROM dfn_macro_files_gen(3); -- 0, 1, 2
SELECT dfn_macro_files_negate(7); -- -7
之所以说这是最便捷的方式:
- 宏就是真正的 SQL 文件,编辑器能高亮和检查;一个文件里可以定义任意多个宏 —— 标量宏与表宏并存,
这是单个
SqlMacro表达不了的。 - 路径相对调用宏的
.rs文件解析,编译期由include_str!内联,因此路径写错是编译错误, 而不是运行时少一个宏。 - 文件按书写顺序执行,后面的文件可以依赖前面定义的宏。
只有一个文件时,include_str! 能达到同样效果,且不需要宏:
#[duck_sql_macro]
pub fn dfn_macro_inc_script() -> &'static str {
include_str!("sql/macro_inc.sql")
}
sql/macro_inc.sql:
CREATE OR REPLACE MACRO dfn_macro_inc_add(a, b) AS (a + b);
CREATE OR REPLACE MACRO dfn_macro_inc_triple(x) AS (x * 3);
CREATE OR REPLACE MACRO dfn_macro_inc_gen(n) AS TABLE SELECT * FROM range(n);
SELECT dfn_macro_inc_add(2, 3); -- 5
SELECT dfn_macro_inc_triple(4); -- 12
SELECT * FROM dfn_macro_inc_gen(3); -- 0, 1, 2
用 SqlMacro 构造宏
宏体很短、并且希望它和 Rust 代码放在一起时,可以直接构造。SqlMacro 来自 quack-rs:
#[duck_sql_macro]
pub fn dfn_macro_clamp() -> SqlMacro {
SqlMacro::scalar("dfn_macro_clamp", &["x", "lo", "hi"], "greatest(lo, least(hi, x))")
}
SELECT dfn_macro_clamp(12, 0, 10); -- 10
SELECT dfn_macro_clamp(-5, 0, 10); -- 0
SELECT typeof(dfn_macro_clamp(5, 0, 10)); -- INTEGER
SqlMacro::scalar(name, params, body) 与 SqlMacro::table(name, params, sql) 分别覆盖两类宏:
#[duck_sql_macro]
pub fn dfn_macro_gen() -> DuckResult<SqlMacro> {
Ok(SqlMacro::table("dfn_macro_gen", &["n"], "SELECT * FROM range(n)"))
}
SELECT * FROM dfn_macro_gen(3); -- 0, 1, 2
SELECT range FROM DFN_MACRO_GEN(2); -- 0, 1(宏名大小写不敏感)
宏体也不限于标量表达式 —— dfn_macro_pair 返回 STRUCT,dfn_macro_mklist 返回 LIST:
SELECT CAST(dfn_macro_pair(5) AS VARCHAR); -- {'a': 5, 'b': 10}
SELECT typeof(dfn_macro_pair(5)); -- STRUCT(a INTEGER, b INTEGER)
SELECT dfn_macro_pair(5).a; -- 5
SELECT CAST(dfn_macro_mklist(5) AS VARCHAR); -- [5, 10]
直接返回 SQL 字符串
返回字符串会原样执行,这也是直接使用 CREATE MACRO 的方式:
#[duck_sql_macro]
pub fn dfn_macro_double() -> String {
"CREATE OR REPLACE MACRO dfn_macro_double(x) AS (x * 2)".to_string()
}
SELECT dfn_macro_double(21); -- 42
一个字符串里可以放用 ; 分隔的多条语句,因此一个函数就能定义有依赖关系的宏:
#[duck_sql_macro]
pub fn dfn_macro_quad() -> DuckResult<String> {
Ok("
CREATE OR REPLACE MACRO dfn_macro_double(x) AS (x * 2);
CREATE OR REPLACE MACRO dfn_macro_quad(x) AS (dfn_macro_double(dfn_macro_double(x)));
".to_string())
}
SELECT dfn_macro_quad(3); -- 12
可接受的四种返回形态是:SqlMacro、DuckResult<SqlMacro>、SQL 字符串(String 或 &'static str),
以及 SQL 字符串的 DuckResult。
错误
宏名按大小写不敏感查找,出错时都是普通的 DuckDB 错误:
| 错误用法 | 报错 |
|---|---|
| 参数个数不对 | dfn_macro_add does not support the supplied arguments |
| 把表宏当标量函数用 | dfn_macro_gen is a table function but it was used as a scalar function |
把标量宏用在 FROM 后 | Table Function with name dfn_macro_add does not exist |
源码与测试
test/extension/functions/sql_macro.rs—— 用 Rust 构造的宏test/extension/functions/sql/—— 那批.sql文件test/sql/functions/sql_macro.test—— 期望结果src/functions/sql_macro_adapter.rs—— 运行时侧