Skip to content

Commit 80841fd

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
1 parent 14732bf commit 80841fd

19 files changed

Lines changed: 1601 additions & 4 deletions

Makefile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,7 @@ REGRESS = scan \
179179
jsonb_operators \
180180
list_comprehension \
181181
predicate_functions \
182+
age_reduce \
182183
map_projection \
183184
direct_field_access \
184185
security \

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

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -800,3 +800,25 @@ ALTER OPERATOR ag_catalog.?&(agtype, text[])
800800
SET (RESTRICT = contsel, JOIN = contjoinsel);
801801
ALTER OPERATOR ag_catalog.?&(agtype, agtype)
802802
SET (RESTRICT = contsel, JOIN = contjoinsel);
803+
804+
--
805+
-- reduce(acc = init, var IN list | body) fold support
806+
--
807+
-- Transition function for the age_reduce aggregate. The fold body is compiled
808+
-- by transform_cypher_reduce() with the accumulator and element rewritten to
809+
-- PARAM_EXEC params 0 and 1 and serialized into the text argument; the
810+
-- transition evaluates it for each element in list order. It must be callable
811+
-- with a NULL transition state (no initcond), so it is intentionally not STRICT.
812+
CREATE FUNCTION ag_catalog.age_reduce_transfn(agtype, agtype, text, agtype)
813+
RETURNS agtype
814+
LANGUAGE c
815+
PARALLEL UNSAFE
816+
AS 'MODULE_PATHNAME';
817+
818+
-- aggregate definition for reduce(); direct arguments are
819+
-- (init, serialized-body, element), with the element fed ORDER BY ordinality.
820+
CREATE AGGREGATE ag_catalog.age_reduce(agtype, text, agtype)
821+
(
822+
stype = agtype,
823+
sfunc = ag_catalog.age_reduce_transfn
824+
);

0 commit comments

Comments
 (0)