Skip to content

Commit 5cff96d

Browse files
authored
Merge pull request IvorySQL#1203 from pierreforstmann/master
add LISTAGG aggregate
2 parents 69ebb6c + 8f0387e commit 5cff96d

8 files changed

Lines changed: 328 additions & 4 deletions

File tree

‎contrib/ivorysql_ora/src/builtin_functions/builtin_functions--1.0.sql‎

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1441,4 +1441,20 @@ LANGUAGE C
14411441
STRICT
14421442
PARALLEL SAFE
14431443
STABLE;
1444-
/* End - ASCII */
1444+
/* End - ASCII */
1445+
1446+
/*
1447+
* LISTAGG support
1448+
*
1449+
* sys.ora_listagg_check(text) enforces Oracle's VARCHAR2 maximum length (4000 bytes)
1450+
* on the LISTAGG result. It is automatically injected by the Oracle-mode parser
1451+
* as a wrapper around string_agg() when LISTAGG(...) WITHIN GROUP (ORDER BY ...)
1452+
* is parsed, so users never call it directly.
1453+
*/
1454+
CREATE FUNCTION sys.ora_listagg_check(text)
1455+
RETURNS text
1456+
AS 'MODULE_PATHNAME', 'ora_listagg_check'
1457+
LANGUAGE C
1458+
CALLED ON NULL INPUT
1459+
PARALLEL SAFE
1460+
IMMUTABLE;

‎contrib/ivorysql_ora/src/builtin_functions/character_datatype_functions.c‎

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ PG_FUNCTION_INFO_V1(ora_asciistr);
7575
PG_FUNCTION_INFO_V1(ora_to_multi_byte);
7676
PG_FUNCTION_INFO_V1(ora_to_single_byte);
7777
PG_FUNCTION_INFO_V1(ora_ascii);
78+
PG_FUNCTION_INFO_V1(ora_listagg_check);
7879

7980
#define PG_STR_GET_TEXT(str_) \
8081
DatumGetTextP(DirectFunctionCall1(textin, CStringGetDatum(str_)))
@@ -854,13 +855,14 @@ ora_regexp_count(PG_FUNCTION_ARGS)
854855
PG_RETURN_INT32(occurrence_cnt);
855856
}
856857

857-
/********************************************************************
858+
/***********************************************************************
858859
* ora_substrb
859860
*
860861
* Purpose:
861862
* Return a substring starting at the specified position in bytes.
862863
*
863-
********************************************************************/
864+
**********************************************************************
865+
*/
864866
Datum
865867
ora_substrb(PG_FUNCTION_ARGS)
866868
{
@@ -2441,3 +2443,39 @@ ora_ascii(PG_FUNCTION_ARGS)
24412443

24422444
PG_RETURN_INT32((unsigned char) str[0]);
24432445
}
2446+
2447+
2448+
/*******************************************************************
2449+
* ora_listagg_check
2450+
*
2451+
* Purpose:
2452+
* Check that LISTAGG does not return string greater than 4000 bytes
2453+
*
2454+
* if yes, returns input parameter
2455+
* otherwise raise error
2456+
*
2457+
*******************************************************************/
2458+
Datum
2459+
ora_listagg_check (PG_FUNCTION_ARGS)
2460+
{
2461+
if (PG_ARGISNULL(0))
2462+
{
2463+
PG_RETURN_NULL();
2464+
}
2465+
else
2466+
{
2467+
text *value = PG_GETARG_TEXT_PP(0);
2468+
int len = VARSIZE_ANY_EXHDR(value); // payload length only
2469+
2470+
if (len <= 4000)
2471+
{
2472+
PG_RETURN_TEXT_P(value);
2473+
}
2474+
else
2475+
{
2476+
elog(ERROR, "result of aggregation exceeds 4000 bytes");
2477+
}
2478+
}
2479+
2480+
}
2481+

‎contrib/ivorysql_ora/src/builtin_functions/misc_functions.c‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
#include "miscadmin.h"
3333
#include "utils/formatting.h"
3434
#include "utils/numeric.h"
35+
#include "varatt.h"
3536

3637
PG_FUNCTION_INFO_V1(uid);
3738

‎src/backend/oracle_parser/ora_gram.y‎

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -853,6 +853,8 @@ static void determineLanguage(List *options);
853853

854854
%token <keyword> PARAMSLENGTH
855855

856+
%token <keyword> LISTAGG
857+
856858
%type <node> CreatePackageStmt CreatePackageBodyStmt AlterPackageStmt
857859
%type <node> package_proper_item
858860
%type <list> package_proper package_proper_list package_names package_names_list
@@ -18002,6 +18004,40 @@ func_expr: func_application within_group_clause filter_clause null_treatment ove
1800218004
}
1800318005
| func_expr_common_subexpr
1800418006
{ $$ = $1; }
18007+
18008+
18009+
| LISTAGG '(' func_arg_list ')' within_group_clause filter_clause over_clause
18010+
{
18011+
FuncCall *string_agg_n;
18012+
FuncCall *check_n;
18013+
18014+
if ($5 == NIL)
18015+
ereport(ERROR,
18016+
(errcode(ERRCODE_SYNTAX_ERROR),
18017+
errmsg("LISTAGG requires WITHIN GROUP (ORDER BY ...)"),
18018+
parser_errposition(@1)));
18019+
/*
18020+
* If no delimiter provided (single arg), append empty string
18021+
* so string_agg always receives exactly 2 arguments.
18022+
*/
18023+
if (list_length($3) == 1)
18024+
$3 = lappend($3, makeStringConst("", @3));
18025+
18026+
string_agg_n = makeFuncCall(SystemFuncName("string_agg"),
18027+
$3,
18028+
COERCE_EXPLICIT_CALL, @1);
18029+
string_agg_n->agg_order = $5;
18030+
string_agg_n->agg_filter = $6;
18031+
string_agg_n->over = $7;
18032+
18033+
/* Wrap with sys.listagg_check to enforce the 4000-byte limit */
18034+
check_n = makeFuncCall(OracleSystemFuncName("ora_listagg_check"),
18035+
list_make1((Node *) string_agg_n),
18036+
COERCE_EXPLICIT_CALL, @1);
18037+
$$ = (Node *) check_n;
18038+
18039+
}
18040+
1800518041
;
1800618042

1800718043
/*
@@ -20821,6 +20857,7 @@ reserved_keyword:
2082120857
| LATERAL_P
2082220858
| LEADING
2082320859
| LIMIT
20860+
| LISTAGG
2082420861
| LOCALTIME
2082520862
| LOCALTIMESTAMP
2082620863
| NAN_P
@@ -21107,6 +21144,7 @@ bare_label_keyword:
2110721144
| LEFT
2110821145
| LEVEL
2110921146
| LIKE
21147+
| LISTAGG
2111021148
| LISTEN
2111121149
| LOAD
2111221150
| LOCAL

‎src/include/oracle_parser/ora_kwlist.h‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,7 @@ PG_KEYWORD("left", LEFT, TYPE_FUNC_NAME_KEYWORD, BARE_LABEL)
297297
PG_KEYWORD("level", LEVEL, UNRESERVED_KEYWORD, BARE_LABEL)
298298
PG_KEYWORD("like", LIKE, TYPE_FUNC_NAME_KEYWORD, BARE_LABEL)
299299
PG_KEYWORD("limit", LIMIT, RESERVED_KEYWORD, AS_LABEL)
300+
PG_KEYWORD("listagg", LISTAGG, RESERVED_KEYWORD, BARE_LABEL)
300301
PG_KEYWORD("listen", LISTEN, UNRESERVED_KEYWORD, BARE_LABEL)
301302
PG_KEYWORD("load", LOAD, UNRESERVED_KEYWORD, BARE_LABEL)
302303
PG_KEYWORD("local", LOCAL, UNRESERVED_KEYWORD, BARE_LABEL)
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
--
2+
-- ORA_LISTAGG
3+
-- Test Oracle-compatible LISTAGG aggregate function
4+
-- Syntax: LISTAGG(measure_expr [, 'delimiter']) WITHIN GROUP (ORDER BY sort_expr)
5+
--
6+
-- Setup
7+
CREATE TABLE listagg_test (
8+
deptno INTEGER,
9+
ename varchar2(20),
10+
sal NUMERIC
11+
);
12+
INSERT INTO listagg_test VALUES
13+
(10, 'CLARK', 2450),
14+
(10, 'KING', 5000),
15+
(10, 'MILLER', 1300),
16+
(20, 'ADAMS', 1100),
17+
(20, 'FORD', 3000),
18+
(20, 'JONES', 2975),
19+
(30, 'ALLEN', 1600),
20+
(30, 'BLAKE', 2850),
21+
(30, 'MARTIN', 1250);
22+
-- Test 1: basic ordered concatenation (measure == sort key)
23+
SELECT LISTAGG(ename, ',') WITHIN GROUP (ORDER BY ename)
24+
FROM listagg_test WHERE deptno = 10;
25+
ora_listagg_check
26+
-------------------
27+
CLARK,KING,MILLER
28+
(1 row)
29+
30+
-- Test 2: sort column differs from measure column (sort by salary DESC)
31+
SELECT LISTAGG(ename, ', ') WITHIN GROUP (ORDER BY sal DESC)
32+
FROM listagg_test WHERE deptno = 10;
33+
ora_listagg_check
34+
---------------------
35+
KING, CLARK, MILLER
36+
(1 row)
37+
38+
-- Test 3: grouped aggregation
39+
SELECT deptno, LISTAGG(ename, '|') WITHIN GROUP (ORDER BY ename)
40+
FROM listagg_test GROUP BY deptno ORDER BY deptno;
41+
deptno | ora_listagg_check
42+
--------+--------------------
43+
10 | CLARK|KING|MILLER
44+
20 | ADAMS|FORD|JONES
45+
30 | ALLEN|BLAKE|MARTIN
46+
(3 rows)
47+
48+
-- Test 4: no delimiter (single-argument form, defaults to empty string)
49+
SELECT LISTAGG(ename) WITHIN GROUP (ORDER BY ename)
50+
FROM listagg_test WHERE deptno = 10;
51+
ora_listagg_check
52+
-------------------
53+
CLARKKINGMILLER
54+
(1 row)
55+
56+
-- Test 5: NULL measure values are ignored (string_agg skips NULLs)
57+
INSERT INTO listagg_test VALUES (10, NULL, 999);
58+
SELECT LISTAGG(ename, ',') WITHIN GROUP (ORDER BY ename)
59+
FROM listagg_test WHERE deptno = 10;
60+
ora_listagg_check
61+
-------------------
62+
CLARK,KING,MILLER
63+
(1 row)
64+
65+
-- Test 6: multi-column ORDER BY
66+
SELECT LISTAGG(ename, ',') WITHIN GROUP (ORDER BY deptno, ename)
67+
FROM listagg_test;
68+
ora_listagg_check
69+
-------------------------------------------------------
70+
CLARK,KING,MILLER,ADAMS,FORD,JONES,ALLEN,BLAKE,MARTIN
71+
(1 row)
72+
73+
-- Test 7: empty result set returns NULL
74+
SELECT LISTAGG(ename, ',') WITHIN GROUP (ORDER BY ename)
75+
FROM listagg_test WHERE deptno = 99;
76+
ora_listagg_check
77+
-------------------
78+
79+
(1 row)
80+
81+
-- Test 8: schema-qualified call
82+
SELECT sys.ora_listagg_check(string_agg(ename, ',' ORDER BY ename))
83+
FROM listagg_test WHERE deptno = 10;
84+
ora_listagg_check
85+
-------------------
86+
CLARK,KING,MILLER
87+
(1 row)
88+
89+
-- Test 9: aggregate with FILTER clause
90+
SELECT LISTAGG(ename, ',') WITHIN GROUP (ORDER BY ename)
91+
FILTER (WHERE sal > 1500)
92+
FROM listagg_test WHERE deptno IS NOT NULL;
93+
ora_listagg_check
94+
-----------------------------------
95+
ALLEN,BLAKE,CLARK,FORD,JONES,KING
96+
(1 row)
97+
98+
-- Test 10: 4000-byte overflow raises an error
99+
DO $$
100+
BEGIN
101+
PERFORM sys.ora_listagg_check(repeat('x', 4001));
102+
RAISE EXCEPTION 'Expected error was not raised';
103+
EXCEPTION
104+
WHEN string_data_right_truncation THEN
105+
RAISE NOTICE 'ORA-01489: result of string concatenation is too long (expected)';
106+
END;
107+
$$;
108+
ERROR: result of aggregation exceeds 4000 bytes
109+
CONTEXT: SQL statement "SELECT sys.ora_listagg_check(repeat('x', 4001))"
110+
PL/iSQL function inline_code_block line 3 at PERFORM
111+
-- Test 11: exactly 4000 bytes is allowed
112+
SELECT length(sys.ora_listagg_check(repeat('x', 4000)));
113+
length
114+
--------
115+
4000
116+
(1 row)
117+
118+
-- Test 12: LISTAGG with GROUP BY and multi-column ORDER BY
119+
SELECT deptno,
120+
LISTAGG(ename ORDER by ename ) WITHIN GROUP(ORDER BY ename) AS employees
121+
FROM listagg_test
122+
GROUP BY deptno
123+
ORDER BY deptno;
124+
ERROR: syntax error at or near "ORDER"
125+
LINE 2: LISTAGG(ename ORDER by ename ) WITHIN GROUP(ORDER BY ...
126+
^
127+
-- Test 13: LISTAGG with GROUP BY and multi-column ORDER BY, with delimiter
128+
SELECT deptno,
129+
LISTAGG(ename, ',' ORDER BY ename) WITHIN GROUP(ORDER BY ename) AS employees
130+
FROM listagg_test
131+
GROUP BY deptno
132+
ORDER BY deptno;
133+
ERROR: syntax error at or near "ORDER"
134+
LINE 2: LISTAGG(ename, ',' ORDER BY ename) WITHIN GROUP(ORDER...
135+
^
136+
-- Cleanup
137+
DROP TABLE listagg_test;

‎src/oracle_test/regress/parallel_schedule‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,7 @@ test: ora_force_view
165165
# Oracle ROWNUM pseudocolumn
166166
# ----------
167167

168-
test: rownum
168+
test: rownum ora_listagg
169169

170170
# ----------
171171
# Oracle identifier syntax (#1002)

0 commit comments

Comments
 (0)