Skip to content

Commit 019da21

Browse files
committed
support STRAGG function
1 parent 925d841 commit 019da21

6 files changed

Lines changed: 642 additions & 0 deletions

File tree

CN/modules/ROOT/nav.adoc

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
** xref:master/oracle_compatibility/compat_read_only_view.adoc[20、视图只读]
3030
** xref:master/oracle_compatibility/with_function_procedure.adoc[21、WITH FUNCTION/PROCEDURE]
3131
** xref:master/oracle_compatibility/compat_create_index_online.adoc[22、索引 ONLINE 参数]
32+
** xref:master/oracle_compatibility/compat_stragg.adoc[23、STRAGG 函数]
3233
* 容器化与云服务
3334
** 容器化指南
3435
*** xref:master/containerization/k8s_deployment.adoc[K8S部署]
@@ -101,6 +102,7 @@
101102
**** xref:master/oracle_builtin_functions/sys_context.adoc[sys_context]
102103
**** xref:master/oracle_builtin_functions/userenv.adoc[userenv]
103104
**** xref:master/oracle_builtin_functions/rawtohex.adoc[rawtohex]
105+
**** xref:master/oracle_builtin_functions/stragg.adoc[stragg]
104106
*** xref:master/gb18030.adoc[国标GB18030]
105107
* 参考指南
106108
** xref:master/tools_reference.adoc[工具参考]
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
:sectnums:
2+
:sectnumlevels: 5
3+
4+
:imagesdir: ./_images
5+
6+
= STRAGG 聚合函数的实现
7+
8+
== 目的
9+
10+
IvorySQL 在 `contrib/ivorysql_ora` 扩展中新增 `sys.stragg(text)` 聚合函数,
11+
实现与 Oracle 同名函数一致的字符串聚合行为:将组内所有非 NULL 值用逗号连接为一个字符串。
12+
13+
== 实现说明
14+
15+
=== 状态布局设计
16+
17+
STRAGG 的聚合状态复用 PostgreSQL 内置 `string_agg` 所使用的 `StringInfo` 布局,
18+
从而可以直接引用 `string_agg_finalfn`、`string_agg_combine`、
19+
`string_agg_serialize`、`string_agg_deserialize` 四个内置函数,
20+
无需另行实现 finalize、并行合并及序列化逻辑。
21+
22+
`StringInfo` 状态的约定如下:
23+
24+
[source,c]
25+
----
26+
/*
27+
* data = "," + val1 + "," + val2 + ...
28+
* 首元素前也预置一个逗号,便于 finalfn 统一处理
29+
* cursor = 1
30+
* 记录前导分隔符的字节长度(逗号占 1 字节)
31+
* string_agg_finalfn 返回 &data[cursor],即自动去掉前导逗号
32+
*/
33+
----
34+
35+
=== 转换函数(`stragg_transfn`)
36+
37+
转换函数位于
38+
`contrib/ivorysql_ora/src/builtin_functions/misc_functions.c`。
39+
40+
[source,c]
41+
----
42+
PG_FUNCTION_INFO_V1(stragg_transfn);
43+
44+
Datum
45+
stragg_transfn(PG_FUNCTION_ARGS)
46+
{
47+
StringInfo state;
48+
MemoryContext aggcontext;
49+
MemoryContext oldcontext;
50+
51+
if (!AggCheckCallContext(fcinfo, &aggcontext))
52+
elog(ERROR, "stragg_transfn called in non-aggregate context");
53+
54+
state = PG_ARGISNULL(0) ? NULL : (StringInfo) PG_GETARG_POINTER(0);
55+
56+
/* 跳过 NULL 输入,与 Oracle STRAGG 行为一致 */
57+
if (!PG_ARGISNULL(1))
58+
{
59+
text *value = PG_GETARG_TEXT_PP(1);
60+
61+
if (state == NULL)
62+
{
63+
oldcontext = MemoryContextSwitchTo(aggcontext);
64+
state = makeStringInfo();
65+
MemoryContextSwitchTo(oldcontext);
66+
67+
/* 首个值:预置分隔符,cursor 记录其长度 */
68+
appendStringInfoChar(state, ',');
69+
state->cursor = 1;
70+
}
71+
else
72+
{
73+
appendStringInfoChar(state, ',');
74+
}
75+
76+
appendBinaryStringInfo(state, VARDATA_ANY(value), VARSIZE_ANY_EXHDR(value));
77+
}
78+
79+
if (state)
80+
PG_RETURN_POINTER(state);
81+
PG_RETURN_NULL();
82+
}
83+
----
84+
85+
关键设计点:
86+
87+
* 状态在聚合上下文(`aggcontext`)中分配,生命周期覆盖整个聚合过程。
88+
* `StringInfo` 内部缓冲区按需翻倍扩容,追加操作均摊 O(1),总时间复杂度 O(N),
89+
优于纯 SQL 拼接方案的 O(N²)。
90+
* NULL 输入由 `PG_ARGISNULL(1)` 判断并跳过,状态不受污染。
91+
92+
=== SQL 定义
93+
94+
转换函数和聚合定义位于
95+
`contrib/ivorysql_ora/src/builtin_functions/builtin_functions--1.0.sql`。
96+
97+
[source,sql]
98+
----
99+
CREATE FUNCTION sys.stragg_transfn(internal, text)
100+
RETURNS internal
101+
AS 'MODULE_PATHNAME', 'stragg_transfn'
102+
LANGUAGE C
103+
CALLED ON NULL INPUT
104+
PARALLEL SAFE;
105+
106+
CREATE AGGREGATE sys.stragg(text) (
107+
SFUNC = sys.stragg_transfn,
108+
STYPE = internal,
109+
FINALFUNC = string_agg_finalfn,
110+
COMBINEFUNC = string_agg_combine,
111+
SERIALFUNC = string_agg_serialize,
112+
DESERIALFUNC = string_agg_deserialize,
113+
PARALLEL = SAFE
114+
);
115+
----
116+
117+
`FINALFUNC`、`COMBINEFUNC`、`SERIALFUNC`、`DESERIALFUNC` 均直接引用 PostgreSQL
118+
内置的 `string_agg` 系列函数,因为 STRAGG 与 `string_agg` 使用完全相同的
119+
`StringInfo` 状态格式。
120+
121+
=== 并行聚合支持
122+
123+
通过指定 `COMBINEFUNC = string_agg_combine` 和序列化/反序列化函数,
124+
STRAGG 支持 PostgreSQL 的并行聚合执行路径。各并行工作进程独立维护局部
125+
`StringInfo` 状态,最终由 leader 进程通过 `string_agg_combine` 合并。
126+
127+
=== 回归测试
128+
129+
测试文件:`contrib/ivorysql_ora/sql/ora_stragg.sql`,
130+
对应预期输出:`contrib/ivorysql_ora/expected/ora_stragg.out`。
131+
132+
在 `Makefile` 的 `ORA_REGRESS` 列表中已加入 `ora_stragg`,
133+
可通过以下命令运行:
134+
135+
[source,bash]
136+
----
137+
cd contrib/ivorysql_ora
138+
make installcheck
139+
----
Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
:sectnums:
2+
:sectnumlevels: 5
3+
4+
:imagesdir: ./_images
5+
6+
= STRAGG 聚合函数
7+
8+
== 目的
9+
10+
本文档说明 IvorySQL 中 `sys.stragg` 聚合函数的功能,该函数以兼容 Oracle 的方式
11+
将组内多行字符串值拼接为一个逗号分隔的字符串。
12+
13+
== 功能说明
14+
15+
* `sys.stragg(text)` 是一个聚合函数,将同一分组中所有非 NULL 的文本值用逗号(`,`)连接。
16+
* NULL 值被自动忽略,不出现在结果中,也不产生多余的逗号。
17+
* 分组内所有值均为 NULL 或分组为空集时,返回 `NULL`(而非空字符串)。
18+
* 不保证输出顺序;如需确定顺序,可在聚合调用中使用 `ORDER BY` 子句(PostgreSQL 扩展语法)。
19+
* 函数定义于 `sys` 模式,在 Oracle 兼容模式(不需要sys前缀)和 PG 模式下均可使用。
20+
21+
== 语法
22+
23+
[source,sql]
24+
----
25+
sys.stragg(expr [ORDER BY sort_expr [ASC | DESC] [, ...]])
26+
----
27+
28+
[cols="1,3"]
29+
|===
30+
|参数 |说明
31+
32+
|`expr`
33+
|任意可隐式转换为 `text` 的表达式;NULL 值将被跳过。
34+
35+
|`ORDER BY`
36+
|可选,指定组内拼接顺序。省略时顺序不确定。
37+
|===
38+
39+
返回类型:`text`
40+
41+
== 测试用例
42+
43+
=== 测试环境准备
44+
45+
[source,sql]
46+
----
47+
CREATE TABLE stragg_test (dept TEXT, name TEXT);
48+
INSERT INTO stragg_test VALUES ('HR', 'Alice');
49+
INSERT INTO stragg_test VALUES ('HR', 'Bob');
50+
INSERT INTO stragg_test VALUES ('HR', 'Carol');
51+
INSERT INTO stragg_test VALUES ('IT', 'Dave');
52+
INSERT INTO stragg_test VALUES ('IT', 'Eve');
53+
----
54+
55+
=== 基础聚合
56+
57+
[source,sql]
58+
----
59+
-- 单组聚合,使用 ORDER BY 保证结果确定
60+
SELECT sys.stragg(name ORDER BY name) FROM stragg_test WHERE dept = 'HR';
61+
-- 期望:Alice,Bob,Carol
62+
63+
-- 配合 GROUP BY 按部门分组
64+
SELECT dept, sys.stragg(name ORDER BY name)
65+
FROM stragg_test
66+
GROUP BY dept
67+
ORDER BY dept;
68+
-- 期望:
69+
-- HR | Alice,Bob,Carol
70+
-- IT | Dave,Eve
71+
----
72+
73+
=== NULL 值处理
74+
75+
[source,sql]
76+
----
77+
-- 插入一行 NULL 值
78+
INSERT INTO stragg_test VALUES ('HR', NULL);
79+
80+
-- NULL 被忽略,结果与无 NULL 时相同
81+
SELECT sys.stragg(name ORDER BY name) FROM stragg_test WHERE dept = 'HR';
82+
-- 期望:Alice,Bob,Carol
83+
84+
-- 所有值均为 NULL 时返回 NULL
85+
SELECT sys.stragg(name) IS NULL FROM stragg_test WHERE name IS NULL;
86+
-- 期望:t
87+
----
88+
89+
=== 空集处理
90+
91+
[source,sql]
92+
----
93+
-- 无匹配行时返回 NULL
94+
SELECT sys.stragg(name) IS NULL FROM stragg_test WHERE dept = 'NONE';
95+
-- 期望:t
96+
----
97+
98+
=== 单值
99+
100+
[source,sql]
101+
----
102+
-- 组内只有一个值时,结果中不含逗号
103+
SELECT sys.stragg(name) FROM stragg_test WHERE dept = 'IT' AND name = 'Dave';
104+
-- 期望:Dave
105+
----
106+
107+
=== PG 兼容模式下使用
108+
109+
[source,sql]
110+
----
111+
SET ivorysql.compatible_mode = pg;
112+
SELECT sys.stragg(name ORDER BY name) FROM stragg_test WHERE dept = 'HR';
113+
-- 期望:Alice,Bob,Carol
114+
RESET ivorysql.compatible_mode;
115+
----
116+
117+
=== 测试环境清理
118+
119+
[source,sql]
120+
----
121+
DROP TABLE stragg_test;
122+
----
123+
124+
== 与 Oracle 的行为差异
125+
126+
[cols="1,2,2"]
127+
|===
128+
|场景 |Oracle STRAGG |IvorySQL sys.stragg
129+
130+
|空集
131+
|`NULL`
132+
|`NULL`(一致)
133+
134+
|全 NULL 分组
135+
|`NULL`
136+
|`NULL`(一致)
137+
138+
|NULL 值
139+
|忽略
140+
|忽略(一致)
141+
142+
|拼接顺序
143+
|不确定
144+
|不确定;可用 `ORDER BY` 子句指定
145+
146+
|`ORDER BY` 子句
147+
|不支持(官方无此语法)
148+
|支持(PostgreSQL 聚合扩展语法)
149+
|===
150+
151+
== 与 LISTAGG 的比较
152+
153+
[cols="1,2,2"]
154+
|===
155+
|特性 |`LISTAGG`(Oracle / IvorySQL)|`STRAGG`(Oracle / IvorySQL)
156+
157+
|分隔符
158+
|可指定任意分隔符
159+
|固定为逗号
160+
161+
|`ORDER BY`
162+
|通过 `WITHIN GROUP (ORDER BY ...)` 指定
163+
|不保证顺序(IvorySQL 支持 PostgreSQL 聚合 `ORDER BY`)
164+
165+
|结果长度限制
166+
|Oracle 限制 4000 字节(IvorySQL 通过 `ora_listagg_check` 检查)
167+
|无限制
168+
169+
|使用场景
170+
|需要精确控制分隔符和顺序的场合
171+
|快速拼接,历史遗留代码迁移
172+
|===

EN/modules/ROOT/nav.adoc

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
** xref:master/oracle_compatibility/compat_read_only_view.adoc[20、Read Only View]
3030
** xref:master/oracle_compatibility/with_function_procedure_en.adoc[21、WITH FUNCTION/PROCEDURE]
3131
** xref:master/oracle_compatibility/compat_create_index_online.adoc[22、ONLINE Parameter for CREATE INDEX]
32+
** xref:master/oracle_compatibility/compat_stragg.adoc[23、STRAGG function]
3233
* Containerization and Cloud Service
3334
** Containerization
3435
*** xref:master/containerization/k8s_deployment.adoc[K8S deployment]
@@ -101,6 +102,7 @@
101102
*** xref:master/oracle_builtin_functions/sys_context.adoc[sys_context]
102103
*** xref:master/oracle_builtin_functions/userenv.adoc[userenv]
103104
*** xref:master/oracle_builtin_functions/rawtohex.adoc[rawtohex]
105+
*** xref:master/oracle_builtin_functions/stragg.adoc[stragg]
104106
** xref:master/gb18030.adoc[GB18030 Character Set]
105107
* Reference
106108
** xref:master/tools_reference.adoc[Tool Reference]

0 commit comments

Comments
 (0)