Skip to content

Commit 56fb282

Browse files
committed
Add reduce() list folding function
Implement the openCypher reduce(acc = init, var IN list | body) expression, which folds an arbitrary expression over a list, threading an accumulator across the elements in list order. This closes a long-standing gap (reduce() was previously unsupported) and works both at the SQL top level and inside a cypher() RETURN/WHERE. Implementation -------------- reduce() is desugared, at transform time, into a correlated scalar subquery over a new ordered aggregate rather than a new executor node, so no PostgreSQL core changes are required: CASE WHEN list IS NULL THEN NULL ELSE COALESCE((SELECT ag_catalog.age_reduce( <init>, '<serialized-body>'::text, r.elem ORDER BY r.ord) FROM unnest(<list>) WITH ORDINALITY AS r(elem, ord)), <init>) END - A new cypher_reduce extensible node carries the accumulator/element names and the init/list/body expressions (grammar production, keyword, and the copy/out/read serialization plumbing). - The fold body is transformed against a throwaway two-column agtype namespace, its accumulator and element Var references are rewritten to PARAM_EXEC params 0 and 1, and it is serialized with nodeToString() into a text argument. - age_reduce_transfn (a custom agtype aggregate transition function) deserializes and compiles the body once per group with ExecInitExpr, then evaluates it per element with ExecEvalExpr, rebinding the two params. The body is normalized to agtype at transform time so a boolean or other non-agtype result cannot be misread as a by-reference Datum. Semantics --------- - List order is preserved (unnest WITH ORDINALITY + aggregate ORDER BY). - An empty list yields the initial value; a NULL list yields NULL. - The list and initial value may reference outer-query variables (e.g. reduce(total = 0, n IN nodes(p) | total + n.age)); the body may reference only the accumulator and element. - Arithmetic, string, list-building, boolean/comparison (AND/OR/=/>), CASE, and element property-access bodies are all supported. - Outer-variable, query-parameter, nested-reduce, and aggregate references inside the body raise a clean ERRCODE_FEATURE_NOT_SUPPORTED error. - reduce is registered as a safe keyword so it remains usable as a property or map key, preserving backward compatibility. Tests ----- Adds the age_reduce regression test (registered in the install SQL and the upgrade template so age_upgrade passes), covering: arithmetic/product/string folds; order sensitivity; empty/NULL list; NULL element and NULL init; list-building and CASE bodies; boolean and comparison bodies; element property access; multiple and nested (in list/init) reduce(); reduce() in boolean expressions, WHERE, and list comprehensions; folds over collected nodes and node list properties; the not-supported rejections; and reduce as a map key. All multi-row results are ordered. 38/38 installcheck pass. Future work ----------- The body restriction (accumulator and element only) is a property of the standalone expression evaluation and can be relaxed without core changes: - Allow loop-invariant outer-variable and cypher $parameter references in the body by capturing them as additional eager aggregate arguments bound to extra param slots. - Support a nested reduce() inside the body via an SPI-based evaluation fallback for subquery-bearing bodies. Aggregates inside the body remain intentionally unsupported, matching the openCypher specification. Co-authored-by: Copilot <copilot@github.com> modified: Makefile modified: age--1.7.0--y.y.y.sql new file: regress/expected/age_reduce.out new file: regress/sql/age_reduce.sql modified: sql/age_aggregate.sql modified: src/backend/nodes/ag_nodes.c modified: src/backend/nodes/cypher_copyfuncs.c modified: src/backend/nodes/cypher_outfuncs.c modified: src/backend/nodes/cypher_readfuncs.c modified: src/backend/parser/cypher_analyze.c modified: src/backend/parser/cypher_clause.c modified: src/backend/parser/cypher_gram.y modified: src/backend/utils/adt/agtype.c modified: src/include/nodes/ag_nodes.h modified: src/include/nodes/cypher_copyfuncs.h modified: src/include/nodes/cypher_nodes.h modified: src/include/nodes/cypher_outfuncs.h modified: src/include/nodes/cypher_readfuncs.h modified: src/include/parser/cypher_kwlist.h Resolved Conflicts: age--1.7.0--y.y.y.sql
1 parent 581d236 commit 56fb282

19 files changed

Lines changed: 1599 additions & 4 deletions

Makefile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,7 @@ REGRESS = scan \
214214
jsonb_operators \
215215
list_comprehension \
216216
predicate_functions \
217+
age_reduce \
217218
map_projection \
218219
direct_field_access \
219220
security \

age--1.7.0--y.y.y.sql

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1057,3 +1057,23 @@ $function$;
10571057

10581058
COMMENT ON FUNCTION ag_catalog.create_subgraph(name, name, text, text) IS
10591059
'Materializes a new persistent graph as the induced subgraph of from_graph selected by a Cypher node predicate (on n) and relationship predicate (on r); ''*'' keeps all. An edge is kept only if its predicate holds and both endpoints are kept. Returns (node_count, relationship_count).';
1060+
-- reduce(acc = init, var IN list | body) fold support
1061+
--
1062+
-- Transition function for the age_reduce aggregate. The fold body is compiled
1063+
-- by transform_cypher_reduce() with the accumulator and element rewritten to
1064+
-- PARAM_EXEC params 0 and 1 and serialized into the text argument; the
1065+
-- transition evaluates it for each element in list order. It must be callable
1066+
-- with a NULL transition state (no initcond), so it is intentionally not STRICT.
1067+
CREATE FUNCTION ag_catalog.age_reduce_transfn(agtype, agtype, text, agtype)
1068+
RETURNS agtype
1069+
LANGUAGE c
1070+
PARALLEL UNSAFE
1071+
AS 'MODULE_PATHNAME';
1072+
1073+
-- aggregate definition for reduce(); direct arguments are
1074+
-- (init, serialized-body, element), with the element fed ORDER BY ordinality.
1075+
CREATE AGGREGATE ag_catalog.age_reduce(agtype, text, agtype)
1076+
(
1077+
stype = agtype,
1078+
sfunc = ag_catalog.age_reduce_transfn
1079+
);

0 commit comments

Comments
 (0)