From 019da212b440d7845600839f7eca0e74d23b6e8f Mon Sep 17 00:00:00 2001 From: Steven Niu Date: Mon, 15 Jun 2026 08:25:01 +0000 Subject: [PATCH] support STRAGG function --- CN/modules/ROOT/nav.adoc | 2 + .../oracle_builtin_functions/stragg.adoc | 139 ++++++++++++++ .../oracle_compatibility/compat_stragg.adoc | 172 +++++++++++++++++ EN/modules/ROOT/nav.adoc | 2 + .../oracle_builtin_functions/stragg.adoc | 148 +++++++++++++++ .../oracle_compatibility/compat_stragg.adoc | 179 ++++++++++++++++++ 6 files changed, 642 insertions(+) create mode 100644 CN/modules/ROOT/pages/master/oracle_builtin_functions/stragg.adoc create mode 100644 CN/modules/ROOT/pages/master/oracle_compatibility/compat_stragg.adoc create mode 100644 EN/modules/ROOT/pages/master/oracle_builtin_functions/stragg.adoc create mode 100644 EN/modules/ROOT/pages/master/oracle_compatibility/compat_stragg.adoc diff --git a/CN/modules/ROOT/nav.adoc b/CN/modules/ROOT/nav.adoc index 5cad74dd..d0e7ddec 100644 --- a/CN/modules/ROOT/nav.adoc +++ b/CN/modules/ROOT/nav.adoc @@ -29,6 +29,7 @@ ** xref:master/oracle_compatibility/compat_read_only_view.adoc[20、视图只读] ** xref:master/oracle_compatibility/with_function_procedure.adoc[21、WITH FUNCTION/PROCEDURE] ** xref:master/oracle_compatibility/compat_create_index_online.adoc[22、索引 ONLINE 参数] +** xref:master/oracle_compatibility/compat_stragg.adoc[23、STRAGG 函数] * 容器化与云服务 ** 容器化指南 *** xref:master/containerization/k8s_deployment.adoc[K8S部署] @@ -101,6 +102,7 @@ **** xref:master/oracle_builtin_functions/sys_context.adoc[sys_context] **** xref:master/oracle_builtin_functions/userenv.adoc[userenv] **** xref:master/oracle_builtin_functions/rawtohex.adoc[rawtohex] +**** xref:master/oracle_builtin_functions/stragg.adoc[stragg] *** xref:master/gb18030.adoc[国标GB18030] * 参考指南 ** xref:master/tools_reference.adoc[工具参考] diff --git a/CN/modules/ROOT/pages/master/oracle_builtin_functions/stragg.adoc b/CN/modules/ROOT/pages/master/oracle_builtin_functions/stragg.adoc new file mode 100644 index 00000000..522a3016 --- /dev/null +++ b/CN/modules/ROOT/pages/master/oracle_builtin_functions/stragg.adoc @@ -0,0 +1,139 @@ +:sectnums: +:sectnumlevels: 5 + +:imagesdir: ./_images + += STRAGG 聚合函数的实现 + +== 目的 + +IvorySQL 在 `contrib/ivorysql_ora` 扩展中新增 `sys.stragg(text)` 聚合函数, +实现与 Oracle 同名函数一致的字符串聚合行为:将组内所有非 NULL 值用逗号连接为一个字符串。 + +== 实现说明 + +=== 状态布局设计 + +STRAGG 的聚合状态复用 PostgreSQL 内置 `string_agg` 所使用的 `StringInfo` 布局, +从而可以直接引用 `string_agg_finalfn`、`string_agg_combine`、 +`string_agg_serialize`、`string_agg_deserialize` 四个内置函数, +无需另行实现 finalize、并行合并及序列化逻辑。 + +`StringInfo` 状态的约定如下: + +[source,c] +---- +/* + * data = "," + val1 + "," + val2 + ... + * 首元素前也预置一个逗号,便于 finalfn 统一处理 + * cursor = 1 + * 记录前导分隔符的字节长度(逗号占 1 字节) + * string_agg_finalfn 返回 &data[cursor],即自动去掉前导逗号 + */ +---- + +=== 转换函数(`stragg_transfn`) + +转换函数位于 +`contrib/ivorysql_ora/src/builtin_functions/misc_functions.c`。 + +[source,c] +---- +PG_FUNCTION_INFO_V1(stragg_transfn); + +Datum +stragg_transfn(PG_FUNCTION_ARGS) +{ + StringInfo state; + MemoryContext aggcontext; + MemoryContext oldcontext; + + if (!AggCheckCallContext(fcinfo, &aggcontext)) + elog(ERROR, "stragg_transfn called in non-aggregate context"); + + state = PG_ARGISNULL(0) ? NULL : (StringInfo) PG_GETARG_POINTER(0); + + /* 跳过 NULL 输入,与 Oracle STRAGG 行为一致 */ + if (!PG_ARGISNULL(1)) + { + text *value = PG_GETARG_TEXT_PP(1); + + if (state == NULL) + { + oldcontext = MemoryContextSwitchTo(aggcontext); + state = makeStringInfo(); + MemoryContextSwitchTo(oldcontext); + + /* 首个值:预置分隔符,cursor 记录其长度 */ + appendStringInfoChar(state, ','); + state->cursor = 1; + } + else + { + appendStringInfoChar(state, ','); + } + + appendBinaryStringInfo(state, VARDATA_ANY(value), VARSIZE_ANY_EXHDR(value)); + } + + if (state) + PG_RETURN_POINTER(state); + PG_RETURN_NULL(); +} +---- + +关键设计点: + +* 状态在聚合上下文(`aggcontext`)中分配,生命周期覆盖整个聚合过程。 +* `StringInfo` 内部缓冲区按需翻倍扩容,追加操作均摊 O(1),总时间复杂度 O(N), + 优于纯 SQL 拼接方案的 O(N²)。 +* NULL 输入由 `PG_ARGISNULL(1)` 判断并跳过,状态不受污染。 + +=== SQL 定义 + +转换函数和聚合定义位于 +`contrib/ivorysql_ora/src/builtin_functions/builtin_functions--1.0.sql`。 + +[source,sql] +---- +CREATE FUNCTION sys.stragg_transfn(internal, text) +RETURNS internal +AS 'MODULE_PATHNAME', 'stragg_transfn' +LANGUAGE C +CALLED ON NULL INPUT +PARALLEL SAFE; + +CREATE AGGREGATE sys.stragg(text) ( + SFUNC = sys.stragg_transfn, + STYPE = internal, + FINALFUNC = string_agg_finalfn, + COMBINEFUNC = string_agg_combine, + SERIALFUNC = string_agg_serialize, + DESERIALFUNC = string_agg_deserialize, + PARALLEL = SAFE +); +---- + +`FINALFUNC`、`COMBINEFUNC`、`SERIALFUNC`、`DESERIALFUNC` 均直接引用 PostgreSQL +内置的 `string_agg` 系列函数,因为 STRAGG 与 `string_agg` 使用完全相同的 +`StringInfo` 状态格式。 + +=== 并行聚合支持 + +通过指定 `COMBINEFUNC = string_agg_combine` 和序列化/反序列化函数, +STRAGG 支持 PostgreSQL 的并行聚合执行路径。各并行工作进程独立维护局部 +`StringInfo` 状态,最终由 leader 进程通过 `string_agg_combine` 合并。 + +=== 回归测试 + +测试文件:`contrib/ivorysql_ora/sql/ora_stragg.sql`, +对应预期输出:`contrib/ivorysql_ora/expected/ora_stragg.out`。 + +在 `Makefile` 的 `ORA_REGRESS` 列表中已加入 `ora_stragg`, +可通过以下命令运行: + +[source,bash] +---- +cd contrib/ivorysql_ora +make installcheck +---- diff --git a/CN/modules/ROOT/pages/master/oracle_compatibility/compat_stragg.adoc b/CN/modules/ROOT/pages/master/oracle_compatibility/compat_stragg.adoc new file mode 100644 index 00000000..08631b61 --- /dev/null +++ b/CN/modules/ROOT/pages/master/oracle_compatibility/compat_stragg.adoc @@ -0,0 +1,172 @@ +:sectnums: +:sectnumlevels: 5 + +:imagesdir: ./_images + += STRAGG 聚合函数 + +== 目的 + +本文档说明 IvorySQL 中 `sys.stragg` 聚合函数的功能,该函数以兼容 Oracle 的方式 +将组内多行字符串值拼接为一个逗号分隔的字符串。 + +== 功能说明 + +* `sys.stragg(text)` 是一个聚合函数,将同一分组中所有非 NULL 的文本值用逗号(`,`)连接。 +* NULL 值被自动忽略,不出现在结果中,也不产生多余的逗号。 +* 分组内所有值均为 NULL 或分组为空集时,返回 `NULL`(而非空字符串)。 +* 不保证输出顺序;如需确定顺序,可在聚合调用中使用 `ORDER BY` 子句(PostgreSQL 扩展语法)。 +* 函数定义于 `sys` 模式,在 Oracle 兼容模式(不需要sys前缀)和 PG 模式下均可使用。 + +== 语法 + +[source,sql] +---- +sys.stragg(expr [ORDER BY sort_expr [ASC | DESC] [, ...]]) +---- + +[cols="1,3"] +|=== +|参数 |说明 + +|`expr` +|任意可隐式转换为 `text` 的表达式;NULL 值将被跳过。 + +|`ORDER BY` +|可选,指定组内拼接顺序。省略时顺序不确定。 +|=== + +返回类型:`text` + +== 测试用例 + +=== 测试环境准备 + +[source,sql] +---- +CREATE TABLE stragg_test (dept TEXT, name TEXT); +INSERT INTO stragg_test VALUES ('HR', 'Alice'); +INSERT INTO stragg_test VALUES ('HR', 'Bob'); +INSERT INTO stragg_test VALUES ('HR', 'Carol'); +INSERT INTO stragg_test VALUES ('IT', 'Dave'); +INSERT INTO stragg_test VALUES ('IT', 'Eve'); +---- + +=== 基础聚合 + +[source,sql] +---- +-- 单组聚合,使用 ORDER BY 保证结果确定 +SELECT sys.stragg(name ORDER BY name) FROM stragg_test WHERE dept = 'HR'; +-- 期望:Alice,Bob,Carol + +-- 配合 GROUP BY 按部门分组 +SELECT dept, sys.stragg(name ORDER BY name) +FROM stragg_test +GROUP BY dept +ORDER BY dept; +-- 期望: +-- HR | Alice,Bob,Carol +-- IT | Dave,Eve +---- + +=== NULL 值处理 + +[source,sql] +---- +-- 插入一行 NULL 值 +INSERT INTO stragg_test VALUES ('HR', NULL); + +-- NULL 被忽略,结果与无 NULL 时相同 +SELECT sys.stragg(name ORDER BY name) FROM stragg_test WHERE dept = 'HR'; +-- 期望:Alice,Bob,Carol + +-- 所有值均为 NULL 时返回 NULL +SELECT sys.stragg(name) IS NULL FROM stragg_test WHERE name IS NULL; +-- 期望:t +---- + +=== 空集处理 + +[source,sql] +---- +-- 无匹配行时返回 NULL +SELECT sys.stragg(name) IS NULL FROM stragg_test WHERE dept = 'NONE'; +-- 期望:t +---- + +=== 单值 + +[source,sql] +---- +-- 组内只有一个值时,结果中不含逗号 +SELECT sys.stragg(name) FROM stragg_test WHERE dept = 'IT' AND name = 'Dave'; +-- 期望:Dave +---- + +=== PG 兼容模式下使用 + +[source,sql] +---- +SET ivorysql.compatible_mode = pg; +SELECT sys.stragg(name ORDER BY name) FROM stragg_test WHERE dept = 'HR'; +-- 期望:Alice,Bob,Carol +RESET ivorysql.compatible_mode; +---- + +=== 测试环境清理 + +[source,sql] +---- +DROP TABLE stragg_test; +---- + +== 与 Oracle 的行为差异 + +[cols="1,2,2"] +|=== +|场景 |Oracle STRAGG |IvorySQL sys.stragg + +|空集 +|`NULL` +|`NULL`(一致) + +|全 NULL 分组 +|`NULL` +|`NULL`(一致) + +|NULL 值 +|忽略 +|忽略(一致) + +|拼接顺序 +|不确定 +|不确定;可用 `ORDER BY` 子句指定 + +|`ORDER BY` 子句 +|不支持(官方无此语法) +|支持(PostgreSQL 聚合扩展语法) +|=== + +== 与 LISTAGG 的比较 + +[cols="1,2,2"] +|=== +|特性 |`LISTAGG`(Oracle / IvorySQL)|`STRAGG`(Oracle / IvorySQL) + +|分隔符 +|可指定任意分隔符 +|固定为逗号 + +|`ORDER BY` +|通过 `WITHIN GROUP (ORDER BY ...)` 指定 +|不保证顺序(IvorySQL 支持 PostgreSQL 聚合 `ORDER BY`) + +|结果长度限制 +|Oracle 限制 4000 字节(IvorySQL 通过 `ora_listagg_check` 检查) +|无限制 + +|使用场景 +|需要精确控制分隔符和顺序的场合 +|快速拼接,历史遗留代码迁移 +|=== diff --git a/EN/modules/ROOT/nav.adoc b/EN/modules/ROOT/nav.adoc index 33e493d1..25134d36 100644 --- a/EN/modules/ROOT/nav.adoc +++ b/EN/modules/ROOT/nav.adoc @@ -29,6 +29,7 @@ ** xref:master/oracle_compatibility/compat_read_only_view.adoc[20、Read Only View] ** xref:master/oracle_compatibility/with_function_procedure_en.adoc[21、WITH FUNCTION/PROCEDURE] ** xref:master/oracle_compatibility/compat_create_index_online.adoc[22、ONLINE Parameter for CREATE INDEX] +** xref:master/oracle_compatibility/compat_stragg.adoc[23、STRAGG function] * Containerization and Cloud Service ** Containerization *** xref:master/containerization/k8s_deployment.adoc[K8S deployment] @@ -101,6 +102,7 @@ *** xref:master/oracle_builtin_functions/sys_context.adoc[sys_context] *** xref:master/oracle_builtin_functions/userenv.adoc[userenv] *** xref:master/oracle_builtin_functions/rawtohex.adoc[rawtohex] +*** xref:master/oracle_builtin_functions/stragg.adoc[stragg] ** xref:master/gb18030.adoc[GB18030 Character Set] * Reference ** xref:master/tools_reference.adoc[Tool Reference] diff --git a/EN/modules/ROOT/pages/master/oracle_builtin_functions/stragg.adoc b/EN/modules/ROOT/pages/master/oracle_builtin_functions/stragg.adoc new file mode 100644 index 00000000..03cba3dd --- /dev/null +++ b/EN/modules/ROOT/pages/master/oracle_builtin_functions/stragg.adoc @@ -0,0 +1,148 @@ +:sectnums: +:sectnumlevels: 5 + +:imagesdir: ./_images + += Implementation of the STRAGG Aggregate Function + +== Purpose + +IvorySQL adds the `sys.stragg(text)` aggregate function to the `contrib/ivorysql_ora` +extension, implementing string aggregation behavior compatible with Oracle's function of +the same name: concatenating all non-NULL values within a group into a single +comma-separated string. + +== Implementation Notes + +=== State Layout Design + +The STRAGG aggregate reuses the `StringInfo` state layout used by PostgreSQL's built-in +`string_agg`, which allows direct reuse of four built-in functions — +`string_agg_finalfn`, `string_agg_combine`, `string_agg_serialize`, and +`string_agg_deserialize` — without implementing separate finalize, parallel combine, or +serialization logic. + +The `StringInfo` state convention is as follows: + +[source,c] +---- +/* + * data = "," + val1 + "," + val2 + ... + * A leading comma is prepended before the first value so that + * the finalfn can strip it uniformly. + * cursor = 1 + * Stores the byte length of the leading delimiter (one byte for ","). + * string_agg_finalfn returns &data[cursor], automatically dropping + * the leading comma. + */ +---- + +=== Transition Function (`stragg_transfn`) + +The transition function is located in +`contrib/ivorysql_ora/src/builtin_functions/misc_functions.c`. + +[source,c] +---- +PG_FUNCTION_INFO_V1(stragg_transfn); + +Datum +stragg_transfn(PG_FUNCTION_ARGS) +{ + StringInfo state; + MemoryContext aggcontext; + MemoryContext oldcontext; + + if (!AggCheckCallContext(fcinfo, &aggcontext)) + elog(ERROR, "stragg_transfn called in non-aggregate context"); + + state = PG_ARGISNULL(0) ? NULL : (StringInfo) PG_GETARG_POINTER(0); + + /* Skip NULL input values, consistent with Oracle STRAGG behavior */ + if (!PG_ARGISNULL(1)) + { + text *value = PG_GETARG_TEXT_PP(1); + + if (state == NULL) + { + oldcontext = MemoryContextSwitchTo(aggcontext); + state = makeStringInfo(); + MemoryContextSwitchTo(oldcontext); + + /* First value: prepend delimiter and record its length in cursor */ + appendStringInfoChar(state, ','); + state->cursor = 1; + } + else + { + appendStringInfoChar(state, ','); + } + + appendBinaryStringInfo(state, VARDATA_ANY(value), VARSIZE_ANY_EXHDR(value)); + } + + if (state) + PG_RETURN_POINTER(state); + PG_RETURN_NULL(); +} +---- + +Key design points: + +* The state is allocated in the aggregate memory context (`aggcontext`) and lives for + the entire duration of the aggregation. +* The `StringInfo` internal buffer doubles in capacity on demand; append operations are + amortized O(1), giving an overall time complexity of O(N) — superior to the O(N²) of + a pure-SQL string-concatenation approach. +* NULL input is detected via `PG_ARGISNULL(1)` and silently skipped, leaving the + accumulated state unaffected. + +=== SQL Definitions + +The transition function and aggregate are defined in +`contrib/ivorysql_ora/src/builtin_functions/builtin_functions--1.0.sql`. + +[source,sql] +---- +CREATE FUNCTION sys.stragg_transfn(internal, text) +RETURNS internal +AS 'MODULE_PATHNAME', 'stragg_transfn' +LANGUAGE C +CALLED ON NULL INPUT +PARALLEL SAFE; + +CREATE AGGREGATE sys.stragg(text) ( + SFUNC = sys.stragg_transfn, + STYPE = internal, + FINALFUNC = string_agg_finalfn, + COMBINEFUNC = string_agg_combine, + SERIALFUNC = string_agg_serialize, + DESERIALFUNC = string_agg_deserialize, + PARALLEL = SAFE +); +---- + +`FINALFUNC`, `COMBINEFUNC`, `SERIALFUNC`, and `DESERIALFUNC` all reference PostgreSQL's +built-in `string_agg` family directly, because STRAGG uses exactly the same `StringInfo` +state format as `string_agg`. + +=== Parallel Aggregation Support + +By specifying `COMBINEFUNC = string_agg_combine` together with the serialize and +deserialize functions, STRAGG supports PostgreSQL's parallel aggregation execution path. +Each parallel worker maintains its own partial `StringInfo` state independently; the +leader process merges them via `string_agg_combine`. + +=== Regression Tests + +Test file: `contrib/ivorysql_ora/sql/ora_stragg.sql` +Expected output: `contrib/ivorysql_ora/expected/ora_stragg.out` + +`ora_stragg` has been added to the `ORA_REGRESS` list in the `Makefile` and can be run +with: + +[source,bash] +---- +cd contrib/ivorysql_ora +make installcheck +---- diff --git a/EN/modules/ROOT/pages/master/oracle_compatibility/compat_stragg.adoc b/EN/modules/ROOT/pages/master/oracle_compatibility/compat_stragg.adoc new file mode 100644 index 00000000..8ad234fd --- /dev/null +++ b/EN/modules/ROOT/pages/master/oracle_compatibility/compat_stragg.adoc @@ -0,0 +1,179 @@ +:sectnums: +:sectnumlevels: 5 + +:imagesdir: ./_images + += STRAGG Aggregate Function + +== Purpose + +This document describes the `sys.stragg` aggregate function in IvorySQL, which +concatenates multiple string values within a group into a single comma-separated string +in a manner compatible with Oracle. + +== Functional Description + +* `sys.stragg(text)` is an aggregate function that joins all non-NULL text values within + a group using a comma (`,`) as the separator. +* NULL values are silently ignored; they do not appear in the result and do not produce + extra commas. +* When all values in a group are NULL, or when the group is an empty set, the function + returns `NULL` (not an empty string). +* Output order is not guaranteed. To obtain a deterministic order, use an `ORDER BY` + clause inside the aggregate call (a PostgreSQL aggregate extension). +* The function is defined in the `sys` schema and is available in both Oracle + compatibility mode (don't need sys prefix) and PG mode. + +== Syntax + +[source,sql] +---- +sys.stragg(expr [ORDER BY sort_expr [ASC | DESC] [, ...]]) +---- + +[cols="1,3"] +|=== +|Parameter |Description + +|`expr` +|Any expression that can be implicitly cast to `text`. NULL values are skipped. + +|`ORDER BY` +|Optional. Specifies the concatenation order within the group. If omitted, order is + indeterminate. +|=== + +Return type: `text` + +== Test Cases + +=== Test Environment Setup + +[source,sql] +---- +CREATE TABLE stragg_test (dept TEXT, name TEXT); +INSERT INTO stragg_test VALUES ('HR', 'Alice'); +INSERT INTO stragg_test VALUES ('HR', 'Bob'); +INSERT INTO stragg_test VALUES ('HR', 'Carol'); +INSERT INTO stragg_test VALUES ('IT', 'Dave'); +INSERT INTO stragg_test VALUES ('IT', 'Eve'); +---- + +=== Basic Aggregation + +[source,sql] +---- +-- Single-group aggregation with ORDER BY for deterministic output +SELECT sys.stragg(name ORDER BY name) FROM stragg_test WHERE dept = 'HR'; +-- Expected: Alice,Bob,Carol + +-- Aggregation with GROUP BY, grouped by department +SELECT dept, sys.stragg(name ORDER BY name) +FROM stragg_test +GROUP BY dept +ORDER BY dept; +-- Expected: +-- HR | Alice,Bob,Carol +-- IT | Dave,Eve +---- + +=== NULL Value Handling + +[source,sql] +---- +-- Insert a row with a NULL value +INSERT INTO stragg_test VALUES ('HR', NULL); + +-- NULL is ignored; result is the same as without the NULL row +SELECT sys.stragg(name ORDER BY name) FROM stragg_test WHERE dept = 'HR'; +-- Expected: Alice,Bob,Carol + +-- All-NULL input returns NULL +SELECT sys.stragg(name) IS NULL FROM stragg_test WHERE name IS NULL; +-- Expected: t +---- + +=== Empty Set Handling + +[source,sql] +---- +-- No matching rows: returns NULL +SELECT sys.stragg(name) IS NULL FROM stragg_test WHERE dept = 'NONE'; +-- Expected: t +---- + +=== Single Value + +[source,sql] +---- +-- Only one value in the group: result contains no comma +SELECT sys.stragg(name) FROM stragg_test WHERE dept = 'IT' AND name = 'Dave'; +-- Expected: Dave +---- + +=== Usage in PG Compatibility Mode + +[source,sql] +---- +SET ivorysql.compatible_mode = pg; +SELECT sys.stragg(name ORDER BY name) FROM stragg_test WHERE dept = 'HR'; +-- Expected: Alice,Bob,Carol +RESET ivorysql.compatible_mode; +---- + +=== Test Environment Cleanup + +[source,sql] +---- +DROP TABLE stragg_test; +---- + +== Behavioral Differences from Oracle + +[cols="1,2,2"] +|=== +|Scenario |Oracle STRAGG |IvorySQL sys.stragg + +|Empty set +|`NULL` +|`NULL` (compatible) + +|All-NULL group +|`NULL` +|`NULL` (compatible) + +|NULL values +|Ignored +|Ignored (compatible) + +|Concatenation order +|Indeterminate +|Indeterminate; can be fixed with an `ORDER BY` clause + +|`ORDER BY` clause +|Not supported (no official syntax) +|Supported (PostgreSQL aggregate extension syntax) +|=== + +== Comparison with LISTAGG + +[cols="1,2,2"] +|=== +|Feature |`LISTAGG` (Oracle / IvorySQL) |`STRAGG` (Oracle / IvorySQL) + +|Separator +|Any delimiter can be specified +|Fixed comma + +|`ORDER BY` +|Specified via `WITHIN GROUP (ORDER BY ...)` +|Order not guaranteed; IvorySQL supports PostgreSQL aggregate `ORDER BY` + +|Result length limit +|Oracle enforces a 4000-byte limit (IvorySQL checks via `ora_listagg_check`) +|No limit + +|Typical use case +|When precise control of delimiter and order is required +|Quick concatenation; migration of legacy Oracle code +|===