blob: fbe324cd145c35b573fd1a8b66e64791f39f2c26 [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
LOAD 'age';
SET search_path TO ag_catalog;
SELECT create_graph('reduce');
--
-- Basic folds
--
-- sum
SELECT * FROM cypher('reduce', $$
RETURN reduce(s = 0, x IN [1, 2, 3] | s + x)
$$) AS (result agtype);
-- sum of a longer list
SELECT * FROM cypher('reduce', $$
RETURN reduce(s = 0, x IN [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] | s + x)
$$) AS (result agtype);
-- product (factorial)
SELECT * FROM cypher('reduce', $$
RETURN reduce(p = 1, x IN [1, 2, 3, 4, 5] | p * x)
$$) AS (result agtype);
-- non-zero initial accumulator
SELECT * FROM cypher('reduce', $$
RETURN reduce(s = 100, x IN [1, 2, 3] | s + x)
$$) AS (result agtype);
-- single element
SELECT * FROM cypher('reduce', $$
RETURN reduce(s = 0, x IN [42] | s + x)
$$) AS (result agtype);
--
-- List order is significant
--
-- left-associative subtraction: ((((0-1)-2)-3)-4) = -10
SELECT * FROM cypher('reduce', $$
RETURN reduce(s = 0, x IN [1, 2, 3, 4] | s - x)
$$) AS (result agtype);
-- forward string concatenation
SELECT * FROM cypher('reduce', $$
RETURN reduce(s = '', x IN ['a', 'b', 'c'] | s + x)
$$) AS (result agtype);
-- reverse string concatenation (element before accumulator)
SELECT * FROM cypher('reduce', $$
RETURN reduce(s = '', x IN ['a', 'b', 'c'] | x + s)
$$) AS (result agtype);
--
-- Empty and NULL list semantics
--
-- empty list returns the initial value
SELECT * FROM cypher('reduce', $$
RETURN reduce(s = 0, x IN [] | s + x)
$$) AS (result agtype);
-- empty list returns the initial value (non-zero)
SELECT * FROM cypher('reduce', $$
RETURN reduce(s = 999, x IN [] | s + x)
$$) AS (result agtype);
-- NULL list returns NULL
SELECT * FROM cypher('reduce', $$
RETURN reduce(s = 0, x IN null | s + x)
$$) AS (result agtype);
-- empty list with a NULL initial value yields NULL: the list is empty (not
-- null) so the fold runs over zero rows, and COALESCE(<no rows>, init) is
-- COALESCE(NULL, NULL) -> NULL. (Distinct from a NULL *list*, which the outer
-- CASE short-circuits to NULL, and from a non-empty list with a NULL init,
-- which seeds the accumulator with agtype 'null'.)
SELECT * FROM cypher('reduce', $$
RETURN reduce(s = null, x IN [] | s + x)
$$) AS (result agtype);
--
-- NULL handling within the fold
--
-- a NULL element propagates through arithmetic to NULL
SELECT * FROM cypher('reduce', $$
RETURN reduce(s = 0, x IN [1, null, 3] | s + x)
$$) AS (result agtype);
-- NULL initial value
SELECT * FROM cypher('reduce', $$
RETURN reduce(s = null, x IN [1, 2, 3] | s)
$$) AS (result agtype);
-- a body that always evaluates to null yields null, NOT the initial value:
-- every step stores agtype 'null' as the running state, so the final state is
-- a real agtype 'null' and the empty-list COALESCE(..., init) guard must not
-- resurrect the initial value here (the load-bearing fold-to-null vs empty-list
-- distinction)
SELECT * FROM cypher('reduce', $$
RETURN reduce(s = 7, x IN [1, 2, 3] | null)
$$) AS (result agtype);
-- the accumulator legitimately becomes null mid-fold and the body climbs back
-- out of it: element 2 sets the accumulator to null, element 3 produces a fresh
-- non-null value, and element 4 reads that recovered state (999 + 4), proving a
-- null intermediate state does not poison the rest of the fold
SELECT * FROM cypher('reduce', $$
RETURN reduce(s = 0, x IN [1, 2, 3, 4] |
CASE WHEN x = 2 THEN null
WHEN x = 3 THEN 999
ELSE s + x END)
$$) AS (result agtype);
--
-- Errors raised from the fold body propagate cleanly
--
-- a type error in the body (agtype number + map) aborts the statement rather
-- than corrupting the running aggregate state or crashing the backend
SELECT * FROM cypher('reduce', $$
RETURN reduce(s = 0, x IN [1, 2] | s + {a: 1})
$$) AS (result agtype);
-- a runtime arithmetic error in the body (division by zero) likewise aborts
-- the fold; the error surfaces from the standalone per-element evaluator
SELECT * FROM cypher('reduce', $$
RETURN reduce(s = 1, x IN [1, 0, 2] | s / x)
$$) AS (result agtype);
--
-- Building a list with the accumulator
--
-- collect squares
SELECT * FROM cypher('reduce', $$
RETURN reduce(acc = [], x IN [1, 2, 3] | acc + [x * x])
$$) AS (result agtype);
--
-- Value types in the fold
--
-- a float accumulator and float elements
SELECT * FROM cypher('reduce', $$
RETURN reduce(s = 0.0, x IN [1.5, 2.5, 3.0] | s + x)
$$) AS (result agtype);
-- negative numbers
SELECT * FROM cypher('reduce', $$
RETURN reduce(s = 0, x IN [-1, -2, -3] | s + x)
$$) AS (result agtype);
-- a map accumulator passed through unchanged
SELECT * FROM cypher('reduce', $$
RETURN reduce(s = {n: 0}, x IN [1, 2, 3] | s)
$$) AS (result agtype);
-- elements that are themselves lists, indexed in the body
SELECT * FROM cypher('reduce', $$
RETURN reduce(s = 0, x IN [[1, 2], [3, 4], [5, 6]] | s + x[0])
$$) AS (result agtype);
--
-- Function calls in the fold body
--
-- a scalar function applied to the element
SELECT * FROM cypher('reduce', $$
RETURN reduce(s = 0, x IN ['a', 'bb', 'ccc'] | s + size(x))
$$) AS (result agtype);
-- the list itself produced by a function
SELECT * FROM cypher('reduce', $$
RETURN reduce(s = 0, x IN range(1, 5) | s + x)
$$) AS (result agtype);
--
-- Composing reduce() with surrounding expressions
--
-- the reduce() result consumed by another function
SELECT * FROM cypher('reduce', $$
RETURN size(reduce(s = [], x IN [1, 2, 3, 4] | s + [x]))
$$) AS (result agtype);
-- the reduce() result used in a comparison
SELECT * FROM cypher('reduce', $$
RETURN reduce(s = 0, x IN [1, 2, 3] | s + x) = 6
$$) AS (result agtype);
--
-- A conditional body (CASE)
--
-- sum of even elements only
SELECT * FROM cypher('reduce', $$
RETURN reduce(s = 0, x IN [1, 2, 3, 4, 5, 6] | CASE WHEN x % 2 = 0 THEN s + x ELSE s END)
$$) AS (result agtype);
--
-- Boolean and comparison fold bodies
--
-- the body evaluates to a boolean, which is normalized to an agtype boolean
-- (a boolean accumulator is a real Cypher use case for "all"/"any" style folds)
-- logical AND fold: all true?
SELECT * FROM cypher('reduce', $$
RETURN reduce(s = true, x IN [true, true, false] | s AND x)
$$) AS (result agtype);
-- logical OR fold: any true?
SELECT * FROM cypher('reduce', $$
RETURN reduce(s = false, x IN [false, true, false] | s OR x)
$$) AS (result agtype);
-- a comparison body
SELECT * FROM cypher('reduce', $$
RETURN reduce(s = false, x IN [1, 2, 3] | x = 2)
$$) AS (result agtype);
-- "does any element equal 2?" (search fold)
SELECT * FROM cypher('reduce', $$
RETURN reduce(found = false, x IN [1, 2, 3] | found OR x = 2)
$$) AS (result agtype);
-- "are all elements positive?" (using a comparison inside the fold)
SELECT * FROM cypher('reduce', $$
RETURN reduce(s = true, x IN [1, 2, 3] | s AND x > 0)
$$) AS (result agtype);
SELECT * FROM cypher('reduce', $$
RETURN reduce(s = true, x IN [1, -2, 3] | s AND x > 0)
$$) AS (result agtype);
--
-- Property access on the element variable
--
-- sum a field across a list of maps
SELECT * FROM cypher('reduce', $$
RETURN reduce(s = 0, x IN [{n: 1}, {n: 2}, {n: 3}] | s + x.n)
$$) AS (result agtype);
-- concatenate a string field across a list of maps
SELECT * FROM cypher('reduce', $$
RETURN reduce(s = '', x IN [{w: 'a'}, {w: 'b'}, {w: 'c'}] | s + x.w)
$$) AS (result agtype);
--
-- Multiple reduce() in one expression
--
SELECT * FROM cypher('reduce', $$
RETURN reduce(s = 0, x IN [1, 2, 3] | s + x) + reduce(p = 1, y IN [2, 3] | p * y)
$$) AS (result agtype);
--
-- reduce() in a boolean expression
--
SELECT * FROM cypher('reduce', $$
RETURN reduce(s = 0, x IN [1, 2, 3] | s + x) > 5
AND reduce(p = 1, y IN [2, 3] | p * y) < 10
$$) AS (result agtype);
--
-- reduce() nested in the list or initial value of another reduce()
--
-- nesting is allowed in the list and the initial value (both evaluated in the
-- outer context) even though it is rejected inside the fold body.
-- nested reduce() in the list
SELECT * FROM cypher('reduce', $$
RETURN reduce(s = 0, x IN [reduce(a = 0, y IN [1, 2, 3] | a + y), 10] | s + x)
$$) AS (result agtype);
-- nested reduce() in the initial value
SELECT * FROM cypher('reduce', $$
RETURN reduce(s = reduce(a = 0, y IN [1, 2, 3] | a + y), x IN [10, 20] | s + x)
$$) AS (result agtype);
--
-- reduce() over a correlated (per-row) list
--
SELECT * FROM cypher('reduce', $$
UNWIND [[1, 2], [3, 4, 5], []] AS arr
RETURN reduce(s = 0, x IN arr | s + x) AS total
ORDER BY total
$$) AS (result agtype);
--
-- reduce() with the list and initial value bound in an outer clause
--
SELECT * FROM cypher('reduce', $$
WITH [10, 20, 30] AS ns
RETURN reduce(t = 0, n IN ns | t + n)
$$) AS (result agtype);
-- the initial value may reference an outer variable (correlation is allowed
-- in the init and the list, only not in the body)
SELECT * FROM cypher('reduce', $$
WITH 5 AS base
RETURN reduce(s = base, x IN [1, 2, 3] | s + x)
$$) AS (result agtype);
--
-- reduce() nested inside a list comprehension
--
SELECT * FROM cypher('reduce', $$
RETURN [v IN [1, 2, 3] | reduce(s = 0, x IN [v, v, v] | s + x)]
$$) AS (result agtype);
--
-- reduce() in a WHERE clause
--
SELECT * FROM cypher('reduce', $$
UNWIND [[1, 2, 3], [1, 1], [10]] AS l
WITH l WHERE reduce(s = 0, x IN l | s + x) > 3
RETURN l
ORDER BY l
$$) AS (result agtype);
--
-- reduce() over graph data (the canonical Cypher example)
--
SELECT * FROM cypher('reduce', $$
CREATE (:person {name: 'Alice', age: 38}),
(:person {name: 'Bob', age: 25}),
(:person {name: 'Daniel', age: 54})
$$) AS (result agtype);
-- sum the ages of all person nodes
SELECT * FROM cypher('reduce', $$
MATCH (p:person)
WITH collect(p) AS people
RETURN reduce(total = 0, n IN people | total + n.age)
$$) AS (result agtype);
--
-- reduce() over a graph node's list property
--
SELECT * FROM cypher('reduce', $$
CREATE (:bag {name: 'low', vals: [1, 2, 3]}),
(:bag {name: 'mid', vals: [5, 5, 5]}),
(:bag {name: 'high', vals: [10, 20, 30]})
$$) AS (result agtype);
-- filter nodes by a reduce() over their list property
SELECT * FROM cypher('reduce', $$
MATCH (u:bag) WHERE reduce(s = 0, x IN u.vals | s + x) > 10
RETURN u.name
ORDER BY u.name
$$) AS (result agtype);
-- compute a reduce() value per node and order by it
SELECT * FROM cypher('reduce', $$
MATCH (u:bag)
RETURN u.name AS name, reduce(s = 0, x IN u.vals | s + x) AS total
ORDER BY total
$$) AS (name agtype, total agtype);
--
-- Outer references in the fold body
--
-- The body may reference loop-invariant values from the enclosing query: an
-- outer variable, a property of an outer variable, or a cypher() parameter.
-- a plain outer variable in the body
SELECT * FROM cypher('reduce', $$
WITH 5 AS w
RETURN reduce(s = 0, x IN [1, 2, 3] | s + x + w)
$$) AS (result agtype);
-- an outer variable used as a multiplier
SELECT * FROM cypher('reduce', $$
WITH 3 AS factor
RETURN reduce(s = 0, x IN [1, 2, 3] | s + x * factor)
$$) AS (result agtype);
-- two distinct outer variables in the body
SELECT * FROM cypher('reduce', $$
WITH 2 AS a, 100 AS b
RETURN reduce(s = 0, x IN [1, 2, 3] | s + x * a + b)
$$) AS (result agtype);
-- a property of an outer (graph) variable in the body
SELECT * FROM cypher('reduce', $$
MATCH (u:bag) WHERE u.name = 'mid'
RETURN reduce(s = 0, x IN [1, 2, 3] | s + x + u.vals[0])
$$) AS (result agtype);
-- the same outer variable referenced more than once in the body
SELECT * FROM cypher('reduce', $$
WITH 7 AS k
RETURN reduce(s = 0, x IN [1, 2, 3] | s + k + k)
$$) AS (result agtype);
-- a property of an outer map referenced in the body
SELECT * FROM cypher('reduce', $$
WITH {factor: 10} AS m
RETURN reduce(s = 0, x IN [1, 2, 3] | s + x * m.factor)
$$) AS (result agtype);
-- a subexpression that mixes an outer reference with the element: only the
-- loop-invariant part (the outer list) is captured, the element index is not
SELECT * FROM cypher('reduce', $$
WITH [10, 20, 30] AS lookup
RETURN reduce(s = 0, x IN [1, 2, 3] | s + lookup[x - 1])
$$) AS (result agtype);
-- an outer reference inside a CASE branch of the body is captured
SELECT * FROM cypher('reduce', $$
WITH 10 AS w
RETURN reduce(s = 0, x IN [1, 2, 3] | CASE WHEN x % 2 = 0 THEN s + w ELSE s + x END)
$$) AS (result agtype);
-- a NULL outer value propagates through the fold
SELECT * FROM cypher('reduce', $$
WITH null AS w
RETURN reduce(s = 0, x IN [1, 2, 3] | s + x + w)
$$) AS (result agtype);
-- multiple outer captures with a mix of NULL and non-NULL: each is bound to its
-- own slot (the non-NULL multiplier is bound and the NULL still propagates)
SELECT * FROM cypher('reduce', $$
WITH 3 AS a, null AS b
RETURN reduce(s = 0, x IN [1, 2, 3] | s + x * a + b)
$$) AS (result agtype);
-- an outer variable that changes per row is captured per group
SELECT * FROM cypher('reduce', $$
UNWIND [1, 2, 3] AS m
RETURN reduce(s = 0, x IN [1, 2, 3, 4] | s + x * m) AS total
ORDER BY total
$$) AS (result agtype);
--
-- Short-circuit evaluation is preserved for outer references in the body
--
-- Only the outer leaf is captured; operators and CASE/AND/OR branches stay in
-- the body, so a guarded outer sub-expression is not evaluated on a branch
-- that is not taken. Each case below would divide by zero if the whole "1/w"
-- were hoisted into an eagerly evaluated aggregate argument instead.
-- the THEN branch is never taken, so "1/w" is not evaluated (expect 6)
SELECT * FROM cypher('reduce', $$
WITH 0 AS w
RETURN reduce(s = 0, x IN [1, 2, 3] | CASE WHEN false THEN s + 1/w ELSE s + x END)
$$) AS (result agtype);
-- the ELSE branch is never taken, so "1/w" is not evaluated (expect 6)
SELECT * FROM cypher('reduce', $$
WITH 0 AS w
RETURN reduce(s = 0, x IN [1, 2, 3] | CASE WHEN true THEN s + x ELSE s + 1/w END)
$$) AS (result agtype);
-- OR short-circuits once "w = 0" is true, so "1/w > 0" is not evaluated
SELECT * FROM cypher('reduce', $$
WITH 0 AS w
RETURN reduce(s = true, x IN [1, 2, 3] | s AND (w = 0 OR 1/w > 0))
$$) AS (result agtype);
-- AND short-circuits once "w <> 0" is false, so "1/w > 0" is not evaluated
SELECT * FROM cypher('reduce', $$
WITH 0 AS w
RETURN reduce(s = true, x IN [1, 2, 3] | s AND (w <> 0 AND 1/w > 0))
$$) AS (result agtype);
-- coalesce short-circuits: "1/w" is not evaluated when arg 1 is non-null
SELECT * FROM cypher('reduce', $$
WITH 0 AS w
RETURN reduce(s = 0, x IN [1, 2, 3] | s + coalesce(w, 1/w))
$$) AS (result agtype);
-- when the guarded branch is taken, the outer sub-expression is evaluated
-- normally (division by a non-zero outer value): x = 2 -> s + 10/2 (expect 9)
SELECT * FROM cypher('reduce', $$
WITH 2 AS w
RETURN reduce(s = 0, x IN [1, 2, 3] | CASE WHEN x % 2 = 0 THEN s + 10/w ELSE s + x END)
$$) AS (result agtype);
--
-- Not-yet-supported constructs raise a clean feature error
--
-- a nested reduce() in the body (any subquery in the body is unsupported)
SELECT * FROM cypher('reduce', $$
RETURN reduce(s = 0, x IN [1, 2] | s + reduce(t = 0, y IN [x] | t + y))
$$) AS (result agtype);
-- an aggregate function in the body
SELECT * FROM cypher('reduce', $$
RETURN reduce(s = 0, x IN [1, 2] | s + count(x))
$$) AS (result agtype);
--
-- Syntax errors: each required piece of the reduce() form is enforced
--
-- missing "= init"
SELECT * FROM cypher('reduce', $$
RETURN reduce(s, x IN [1, 2] | s + x)
$$) AS (result agtype);
-- missing ", var IN list"
SELECT * FROM cypher('reduce', $$
RETURN reduce(s = 0 | s)
$$) AS (result agtype);
-- missing "| body"
SELECT * FROM cypher('reduce', $$
RETURN reduce(s = 0, x IN [1, 2])
$$) AS (result agtype);
-- a qualified iterator variable is not allowed
SELECT * FROM cypher('reduce', $$
RETURN reduce(s = 0, x.y IN [1, 2] | s)
$$) AS (result agtype);
--
-- cypher() parameter referenced in the fold body (via a prepared statement)
--
PREPARE reduce_param(agtype) AS
SELECT * FROM cypher('reduce', $$
RETURN reduce(s = 0, x IN [1, 2, 3] | s + x + $p)
$$, $1) AS (result agtype);
EXECUTE reduce_param('{"p": 10}');
EXECUTE reduce_param('{"p": 100}');
DEALLOCATE reduce_param;
--
-- "reduce" as a property key name (safe_keywords backward compatibility):
-- because reduce() introduced a reserved keyword, confirm the word is still
-- usable as a map key, the same way any/none/single are.
--
SELECT * FROM cypher('reduce', $$
RETURN {reduce: 1, any: 2, none: 3}
$$) AS (result agtype);
--
-- Cleanup
--
SELECT * FROM drop_graph('reduce', true);