duckfn 0.0.18

Write DuckDB extensions in plain Rust: attribute macros that turn ordinary functions into scalar/aggregate/table functions, SQL macros and nested LIST/MAP/ARRAY/STRUCT types.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
# name: test/sql/functions/scalar_function.test
# description: duck_scalar_function 的返回值形式、入参可空性、参数个数与顺序、注册控制(auto_register / 函数集重载 / overloads_name)、special_null_handling
# group: [functions]

require duckfn

# ============================================================================
# 返回值形式 1/3:Plain -> T(宏包一层 Ok(Some(..)))
# ============================================================================

query I
SELECT dfn_scalar_ret_plain(21);
----
42

query T
SELECT typeof(dfn_scalar_ret_plain(1));
----
INTEGER

# 入参为 NULL 时整行短路(非 Option 参数由 duckfn 参数读取层拦下),函数体不执行
query I
SELECT dfn_scalar_ret_plain(NULL::INTEGER);
----
NULL

query I
SELECT dfn_scalar_ret_plain(i) FROM (VALUES (1), (NULL), (3)) t(i);
----
2
NULL
6

# ============================================================================
# 返回值形式 2/3:Option -> Option<T>,None 落成 SQL NULL
# ============================================================================

query I
SELECT dfn_scalar_ret_option(i) FROM (VALUES (4), (0), (2)) t(i);
----
25
NULL
50

query T
SELECT typeof(dfn_scalar_ret_option(4));
----
INTEGER

# ============================================================================
# 返回值形式 3/3:DuckOptionResult -> DuckOptionResult<T>,既可返回 NULL 也可报错
# ============================================================================

query I
SELECT dfn_scalar_ret_checked(i) FROM (VALUES (4), (-1), (5)) t(i);
----
25
NULL
20

query T
SELECT typeof(dfn_scalar_ret_checked(4));
----
INTEGER

statement error
SELECT dfn_scalar_ret_checked(0);
----
dfn_scalar_ret_checked: division by zero

# 批内某一行报错,整条查询失败
statement error
SELECT dfn_scalar_ret_checked(i) FROM (VALUES (4), (0)) t(i);
----
dfn_scalar_ret_checked: division by zero

# ============================================================================
# panic 传播:函数体 panic 由 duck_scalar_unwind 捕获成查询错误
# ============================================================================

query I
SELECT dfn_scalar_ret_panic(1);
----
1

statement error
SELECT dfn_scalar_ret_panic(13);
----
unlucky input: 13

# ============================================================================
# 入参可空性:非 Option 参数短路整行,Option 参数收到 None
# ============================================================================

# 函数体拿到哨兵值就会 panic,这里没有报错即说明 NULL 行没进入函数体
query I
SELECT dfn_scalar_null_arg_plain(a, b) FROM (VALUES (1, 2), (NULL, 2), (1, NULL), (3, 4)) t(a, b);
----
3
NULL
NULL
7

# Option 入参:NULL 以 None 进入函数体,语义由函数决定(这里返回 -1)
query I
SELECT dfn_scalar_null_arg_option(a) FROM (VALUES (7), (NULL), (8)) t(a);
----
7
-1
8

# 混合入参:非 Option 参数拦下 NULL,Option 参数放行 NULL
query I
SELECT dfn_scalar_null_arg_mixed(a, b) FROM (VALUES (1, 2), (1, NULL), (5, 6)) t(a, b);
----
102
-1
506

# 非 Option 参数为 NULL 时整行短路,后面的 Option 参数根本没被读
query I
SELECT dfn_scalar_null_arg_mixed(a, b) FROM (VALUES (NULL, 2)) t(a, b);
----
NULL

# 整列 NULL:每行都短路
query I
SELECT dfn_scalar_null_arg_all(a) FROM (VALUES (NULL), (NULL)) t(a);
----
NULL
NULL

query I
SELECT dfn_scalar_null_arg_all(NULL::INTEGER);
----
NULL

# ============================================================================
# 参数个数与顺序
# ============================================================================

query I
SELECT dfn_scalar_arity_zero();
----
42

query T
SELECT typeof(dfn_scalar_arity_zero());
----
INTEGER

statement error
SELECT dfn_scalar_arity_zero(1);
----
No function matches

query I
SELECT dfn_scalar_arity_one(1);
----
2

# 多参数:逐个位置匹配类型
query T
SELECT dfn_scalar_arity_three(1, 'x', 2.5);
----
1|x|2.5

query T
SELECT dfn_scalar_arity_order(3, 2, 1);
----
3-2-1

# 第二个参数类型不符
statement error
SELECT dfn_scalar_arity_three(1, 2, 2.5);
----
No function matches

# 参数少于签名
statement error
SELECT dfn_scalar_arity_three(1, 'x');
----
No function matches

# 显式 NULL 不改变签名匹配,只让该行走 NULL
query T
SELECT dfn_scalar_arity_three(NULL::INTEGER, NULL::VARCHAR, NULL::DOUBLE);
----
NULL

# 有效值与 NULL 混合
query T
SELECT dfn_scalar_arity_three(a, b, c) FROM (VALUES (1, 'x', 2.5), (2, NULL, 3.5)) t(a, b, c);
----
1|x|2.5
NULL

# ============================================================================
# 注册控制 1/3:auto_register = false 只生成 builder,不写 inventory 提交
# ============================================================================

# 只声明、不手动注册:SQL 层永远看不到这个名字
statement error
SELECT dfn_scalar_reg_unregistered(1);
----
Scalar Function with name dfn_scalar_reg_unregistered does not exist

# 手动注册(#[duck_custom_register] + scalar_function_builder())后可用
query I
SELECT dfn_scalar_reg_manual(1);
----
2

# ============================================================================
# 注册控制 2/3:函数集重载,一个 SQL 名字挂多个签名
# ============================================================================

query T
SELECT dfn_scalar_reg_overload(1);
----
integer:1

query T
SELECT dfn_scalar_reg_overload('x');
----
varchar:x

# 两个重载分支的函数名本身没有被自动注册
statement error
SELECT dfn_scalar_reg_over_int(1);
----
Scalar Function with name dfn_scalar_reg_over_int does not exist

statement error
SELECT dfn_scalar_reg_over_varchar('x');
----
Scalar Function with name dfn_scalar_reg_over_varchar does not exist

# 重载按参数类型分派,匹配不到就报错
statement error
SELECT dfn_scalar_reg_overload([1, 2]);
----
No function matches

# ============================================================================
# 注册控制:overloads_name —— 宏直接管理函数集重载
#
# 属性写 overloads_name = "函数集名" 的函数不注册自己的名字,而是由宏提交
# duckfn::DuckScalarOverloadItem,register_all_scalar_overload 把
# 同名(函数集名相同)的重载合并成一个函数集。无需手写 #[duck_custom_register]。
# ============================================================================

# 分支 1:INTEGER -> VARCHAR
query T
SELECT dfn_scalar_ovl_set(1);
----
int:1

query T
SELECT typeof(dfn_scalar_ovl_set(1));
----
VARCHAR

# 分支 2:VARCHAR -> BIGINT(返回类型与分支 1 不同)
query I
SELECT dfn_scalar_ovl_set('abcd');
----
4

query T
SELECT typeof(dfn_scalar_ovl_set('abcd'));
----
BIGINT

# 分支 3:INTEGER, INTEGER -> BIGINT(参数个数不同)
query I
SELECT dfn_scalar_ovl_set(3, 4);
----
12

# 三个分支的函数名本身都没有被注册
statement error
SELECT dfn_scalar_ovl_int(1);
----
Scalar Function with name dfn_scalar_ovl_int does not exist

statement error
SELECT dfn_scalar_ovl_varchar('x');
----
Scalar Function with name dfn_scalar_ovl_varchar does not exist

statement error
SELECT dfn_scalar_ovl_int_int(1, 2);
----
Scalar Function with name dfn_scalar_ovl_int_int does not exist

# 参数类型匹配不到就报错
statement error
SELECT dfn_scalar_ovl_set([1, 2]);
----
No function matches

# ============================================================================
# 注册控制 3/3:scalar 只用位置参数,`名字 := 值` 里的名字被 DuckDB 忽略
# ============================================================================

query I
SELECT dfn_scalar_reg_named_param(1, 2);
----
12

query I
SELECT dfn_scalar_reg_named_param(NULL::INTEGER, 2);
----
NULL

# `:=` 的名字对 scalar 没有任何作用:DuckDB 按书写顺序绑定到位置参数,
# 名字与参数名不一致、甚至完全不存在都不报错
query I
SELECT dfn_scalar_reg_named_param(a := 1, b := 2);
----
12

query I
SELECT dfn_scalar_reg_named_param(b := 2, a := 1);
----
21

query I
SELECT dfn_scalar_reg_named_param(x := 1, y := 2);
----
12

# ============================================================================
# special_null_handling:常量 NULL 是否被 DuckDB 折叠
#
# 这是适配层 null_handling() 覆盖唯一能观察到的差异:
#   - 默认(DefaultNullHandling):入参是常量 NULL 时,DuckDB 在 bind 阶段就把
#     结果折叠成常量 NULL,回调不执行 —— 所以拿不到哨兵值 -1;
#   - special_null_handling = true:不折叠,NULL 以 None 进回调。
# 列里的 NULL 两者都会进回调,见下面「列 NULL」一段。
# ============================================================================

# 默认:常量 NULL 直接折叠成 NULL
query I
SELECT dfn_scalar_null_handling_default(NULL::INTEGER);
----
NULL

query I
SELECT dfn_scalar_null_handling_default(NULL::INTEGER) FROM range(3);
----
NULL
NULL
NULL

# special:常量 NULL 不折叠,函数体拿到 None(返回哨兵值 -1)
query I
SELECT dfn_scalar_null_handling_special(NULL::INTEGER);
----
-1

query I
SELECT dfn_scalar_null_handling_special(NULL::INTEGER) FROM range(3);
----
-1
-1
-1

# 判定的是「参数是不是 NULL 常量」,表达式折叠出来的 NULL 也算
query I
SELECT dfn_scalar_null_handling_default(NULL::INTEGER + 0);
----
NULL

query I
SELECT dfn_scalar_null_handling_special(NULL::INTEGER + 0);
----
-1

# 列里的 NULL:两种设置都进函数体(差异只出现在常量折叠这条路径上)
query I
SELECT dfn_scalar_null_handling_default(a) FROM (VALUES (7), (NULL), (8)) t(a);
----
7
-1
8

query I
SELECT dfn_scalar_null_handling_special(a) FROM (VALUES (7), (NULL), (8)) t(a);
----
7
-1
8

# 整列 NULL 同理
query I
SELECT dfn_scalar_null_handling_default(a) FROM (VALUES (NULL::INTEGER), (NULL::INTEGER)) t(a);
----
-1
-1

# special_null_handling 只改 DuckDB 的交参策略;入参写 T 时读取层仍会把
# NULL 短路成 NULL,函数体不执行(哨兵值不会触发 panic)
query I
SELECT dfn_scalar_null_handling_special_plain(a) FROM (VALUES (7), (NULL), (8)) t(a);
----
14
NULL
16

query I
SELECT dfn_scalar_null_handling_special_plain(NULL::INTEGER);
----
NULL

# ============================================================================
# volatile:注册期调用 duckdb_scalar_function_set_volatile
#
# 适配层 volatile() 默认返回 false,注册时不开 volatile;属性写 volatile = true 时
# 宏在 impl 块里覆盖成 true,quack-rs 注册时调用上面那个 FFI 设置函数。
# DuckDB 侧的行为(不缓存 / 不复用相同参数的调用结果)无法从 SQL 直接观察,
# 这里验证的是开关通路没有破坏注册与调用:值、类型都对,且可与其它属性共存。
# ============================================================================

query I
SELECT dfn_scalar_volatile_random(1);
----
2654435762

query T
SELECT typeof(dfn_scalar_volatile_random(1));
----
BIGINT

# 逐行求值:每个 seed 都算一遍
query I
SELECT dfn_scalar_volatile_random(i) FROM (VALUES (0), (1), (2)) t(i);
----
1
2654435762
5308871523

# 常量参数:非 volatile 时 DuckDB 可能折叠成只执行一次,volatile 下每行都重新求值;
# 本函数是纯函数,两者取值相同
query I
SELECT dfn_scalar_volatile_random(1) FROM range(3);
----
2654435762
2654435762
2654435762

# volatile 与 special_null_handling 同用:常量 NULL 不折叠,哨兵值 -1 出现
query I
SELECT dfn_scalar_volatile_special(NULL::INTEGER);
----
-1

query I
SELECT dfn_scalar_volatile_special(NULL::INTEGER) FROM range(3);
----
-1
-1
-1

# ============================================================================
# varargs:注册期调用 duckdb_scalar_function_set_varargs
#
# 属性写 varargs = true 时函数签名最后一个参数必须是 Vec<T>(可变参数集合),
# 宏把 T 的逻辑类型交给上面那个 FFI 设置函数,并把固定参数之后的每一列按 T 读成 Vec<T>。
# 固定参数照常走 DuckArgsImpl;任一非可空固定参数或元素为 NULL 时整行短路为 NULL。
# ============================================================================

# 只有可变参数:所有入参都是 BIGINT
query I
SELECT dfn_scalar_varargs_sum(1, 2, 3);
----
6

query T
SELECT typeof(dfn_scalar_varargs_sum(1, 2, 3));
----
BIGINT

# 零个可变参数也合法,得到空集合
query I
SELECT dfn_scalar_varargs_sum();
----
0

# 逐行读取:range 的每一行都作为「一个可变参数」传进来
query I
SELECT dfn_scalar_varargs_sum(i) FROM range(3) t(i);
----
0
1
2

# 固定参数 + 可变参数,元素类型是 Option<String>(每个可变参数可空)
# 列里的 NULL 会进入函数体,被 flatten 掉
query T
SELECT dfn_scalar_varargs_join(sep, p1, p2)
FROM (VALUES ('-', 'a', NULL), ('+', NULL, 'b'), ('*', 'x', 'y')) t(sep, p1, p2);
----
a
b
x*y

# 只有固定参数、没有可变参数:可变参数集合为空
query I
SELECT length(dfn_scalar_varargs_join('-'));
----
0

# 常量 NULL 与普通参数一样会被 DuckDB 折叠,回调不执行
query T
SELECT dfn_scalar_varargs_join('-', 'a', NULL);
----
NULL

query T
SELECT dfn_scalar_varargs_join(NULL, 'a');
----
NULL

# 非可空元素遇到 NULL:读取层整行短路为 NULL(第二行正常计算)
query I
SELECT dfn_scalar_varargs_sum(a, b, c) FROM (VALUES (1, NULL, 3), (4, 5, 6)) t(a, b, c);
----
NULL
15

# 可变参数本身是 LIST:Vec<Vec<i64>> 对应 varargs_logical(LIST(BIGINT))
query I
SELECT dfn_scalar_varargs_merge([1, 2], [3], []);
----
[1, 2, 3]

query T
SELECT typeof(dfn_scalar_varargs_merge([1]));
----
BIGINT[]