Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions regress/expected/list_comprehension.out
Original file line number Diff line number Diff line change
Expand Up @@ -721,6 +721,39 @@ SELECT * FROM cypher('list_comprehension', $$ MATCH (u {list: [0, 2, 4, 6, 8, 10
{"id": 281474976710668, "label": "", "properties": {"b": [0, 1, 2, 3, 4, 5], "c": [0, 2, 4, 6, 8, 10, 12], "list": [0, 2, 4, 6, 8, 10, 12]}}::vertex
(2 rows)

-- Issue 2393 - WHERE filter over null elements should use openCypher's
-- three-valued logic: IS NULL must keep nulls, IS NOT NULL must drop them.
SELECT * FROM cypher('list_comprehension', $$ RETURN [x IN [null, 1] WHERE x IS NULL] $$) AS (result agtype);
result
--------
[null]
(1 row)

SELECT * FROM cypher('list_comprehension', $$ RETURN [x IN [null, 1, null] WHERE x IS NULL] $$) AS (result agtype);
result
--------------
[null, null]
(1 row)

SELECT * FROM cypher('list_comprehension', $$ RETURN [x IN [null, 1] WHERE x IS NOT NULL] $$) AS (result agtype);
result
--------
[1]
(1 row)

SELECT * FROM cypher('list_comprehension', $$ RETURN [x IN [1, 2, 3] WHERE x IS NULL] $$) AS (result agtype);
result
--------
[]
(1 row)

SELECT * FROM cypher('list_comprehension', $$ UNWIND [null, 1] AS x RETURN x, x IS NULL, x IS NOT NULL $$) AS (x agtype, a agtype, b agtype);
x | a | b
---+-------+-------
| true | false
1 | false | true
(2 rows)

-- Clean up
SELECT * FROM drop_graph('list_comprehension', true);
NOTICE: drop cascades to 4 other objects
Expand Down
116 changes: 100 additions & 16 deletions regress/expected/predicate_functions.out
Original file line number Diff line number Diff line change
Expand Up @@ -194,20 +194,20 @@ $$) AS (result agtype);
--
-- NULL predicate results: three-valued logic
--
-- Note: In AGE's agtype, null is a first-class value. The comparison
-- agtype_null > agtype_integer evaluates to true (not SQL NULL).
-- Three-valued logic only applies when the predicate itself is a
-- literal null constant, which becomes SQL NULL after coercion.
-- agtype null in list: null > 0 = true in AGE, so any() = true
-- Null list elements arrive at the predicate as SQL NULL, so the usual
-- strict-operator short-circuit applies: `null > 0` yields NULL, and the
-- predicate functions combine NULLs with the openCypher three-valued
-- logic (true trumps null in any(), false trumps null in all(), etc.).
-- [null]: only null predicate, no true -> any() = NULL
SELECT * FROM cypher('predicate_functions', $$
RETURN any(x IN [null] WHERE x > 0)
$$) AS (result agtype);
result
--------
true

(1 row)

-- agtype null + real values: all comparisons are true
-- one true (1 > 0) is enough: any() = true
SELECT * FROM cypher('predicate_functions', $$
RETURN any(x IN [null, 1, 2] WHERE x > 0)
$$) AS (result agtype);
Expand All @@ -226,16 +226,16 @@ $$) AS (result agtype);

(1 row)

-- agtype null in list: null > 0 = true in AGE, so all() = true
-- no false, but one null -> all() = NULL
SELECT * FROM cypher('predicate_functions', $$
RETURN all(x IN [1, null, 2] WHERE x > 0)
$$) AS (result agtype);
result
--------
true

(1 row)

-- -1 > 0 = false, so all() = false
-- -1 > 0 = false dominates the null -> all() = false
SELECT * FROM cypher('predicate_functions', $$
RETURN all(x IN [1, null, -1] WHERE x > 0)
$$) AS (result agtype);
Expand All @@ -244,16 +244,16 @@ $$) AS (result agtype);
false
(1 row)

-- agtype null > 0 = true in AGE, so none() = false
-- [null]: only null predicate, no true -> none() = NULL
SELECT * FROM cypher('predicate_functions', $$
RETURN none(x IN [null] WHERE x > 0)
$$) AS (result agtype);
result
--------
false

(1 row)

-- 5 > 0 = true, so none() = false
-- one true (5 > 0) dominates: none() = false
SELECT * FROM cypher('predicate_functions', $$
RETURN none(x IN [null, 5] WHERE x > 0)
$$) AS (result agtype);
Expand All @@ -262,24 +262,108 @@ $$) AS (result agtype);
false
(1 row)

-- agtype null > 0 = true AND 5 > 0 = true: 2 matches, single = false
-- one definite true (5 > 0) and one null predicate: the null could also
-- be a match, so we cannot conclude exactly-one -> single() = NULL
SELECT * FROM cypher('predicate_functions', $$
RETURN single(x IN [null, 5] WHERE x > 0)
$$) AS (result agtype);
result
--------

(1 row)

-- two definite trues dominate any null -> single() = false
SELECT * FROM cypher('predicate_functions', $$
RETURN single(x IN [null, 5, 6] WHERE x > 0)
$$) AS (result agtype);
result
--------
false
(1 row)

-- single() with null list: NULL (same as other predicate functions)
-- only null predicates -> single() = NULL
SELECT * FROM cypher('predicate_functions', $$
RETURN single(x IN null WHERE x > 0)
RETURN single(x IN [null, null] WHERE x > 0)
$$) AS (result agtype);
result
--------

(1 row)

-- one true + one false + one null: the null could be the second true
-- so we cannot conclude exactly-one -> single() = NULL
SELECT * FROM cypher('predicate_functions', $$
RETURN single(x IN [1, null, -1] WHERE x > 0)
$$) AS (result agtype);
result
--------

(1 row)

--
-- Additional null/three-valued coverage for any()/all()/none()
--
-- any() with no true and one null -> NULL
SELECT * FROM cypher('predicate_functions', $$
RETURN any(x IN [null, -1] WHERE x > 0)
$$) AS (result agtype);
result
--------

(1 row)

-- all() with one true and one null -> NULL (the null might be false)
SELECT * FROM cypher('predicate_functions', $$
RETURN all(x IN [1, null] WHERE x > 0)
$$) AS (result agtype);
result
--------

(1 row)

-- all() with one definite false dominates any null -> false
SELECT * FROM cypher('predicate_functions', $$
RETURN all(x IN [null, -1] WHERE x > 0)
$$) AS (result agtype);
result
--------
false
(1 row)

-- none() with no true and one null -> NULL
SELECT * FROM cypher('predicate_functions', $$
RETURN none(x IN [null, -1] WHERE x > 0)
$$) AS (result agtype);
result
--------

(1 row)

-- IS NULL predicate now sees unwound null elements (issue #2393)
SELECT * FROM cypher('predicate_functions', $$
RETURN any(x IN [1, null] WHERE x IS NULL)
$$) AS (result agtype);
result
--------
true
(1 row)

SELECT * FROM cypher('predicate_functions', $$
RETURN none(x IN [1, null] WHERE x IS NULL)
$$) AS (result agtype);
result
--------
false
(1 row)

SELECT * FROM cypher('predicate_functions', $$
RETURN all(x IN [null, null] WHERE x IS NULL)
$$) AS (result agtype);
result
--------
true
(1 row)

--
-- Integration with graph data
--
Expand Down
8 changes: 8 additions & 0 deletions regress/sql/list_comprehension.sql
Original file line number Diff line number Diff line change
Expand Up @@ -174,5 +174,13 @@ SELECT * FROM cypher('list_comprehension', $$ MATCH (u {list: [0, 2, 4, 6, 8, 10
SELECT * FROM cypher('list_comprehension', $$ MATCH (u {list: [0, 2, 4, 6, 8, 10, 12]}) WHERE u.list = [u IN [1, u]] RETURN u $$) AS (u agtype);
SELECT * FROM cypher('list_comprehension', $$ MATCH (u {list: [0, 2, 4, 6, 8, 10, 12]}) WHERE u.list IN [u IN [1, u.list]] RETURN u $$) AS (u agtype);

-- Issue 2393 - WHERE filter over null elements should use openCypher's
-- three-valued logic: IS NULL must keep nulls, IS NOT NULL must drop them.
SELECT * FROM cypher('list_comprehension', $$ RETURN [x IN [null, 1] WHERE x IS NULL] $$) AS (result agtype);
SELECT * FROM cypher('list_comprehension', $$ RETURN [x IN [null, 1, null] WHERE x IS NULL] $$) AS (result agtype);
SELECT * FROM cypher('list_comprehension', $$ RETURN [x IN [null, 1] WHERE x IS NOT NULL] $$) AS (result agtype);
SELECT * FROM cypher('list_comprehension', $$ RETURN [x IN [1, 2, 3] WHERE x IS NULL] $$) AS (result agtype);
SELECT * FROM cypher('list_comprehension', $$ UNWIND [null, 1] AS x RETURN x, x IS NULL, x IS NOT NULL $$) AS (x agtype, a agtype, b agtype);

-- Clean up
SELECT * FROM drop_graph('list_comprehension', true);
75 changes: 62 additions & 13 deletions regress/sql/predicate_functions.sql
Original file line number Diff line number Diff line change
Expand Up @@ -123,17 +123,17 @@ $$) AS (result agtype);
--
-- NULL predicate results: three-valued logic
--
-- Note: In AGE's agtype, null is a first-class value. The comparison
-- agtype_null > agtype_integer evaluates to true (not SQL NULL).
-- Three-valued logic only applies when the predicate itself is a
-- literal null constant, which becomes SQL NULL after coercion.
-- Null list elements arrive at the predicate as SQL NULL, so the usual
-- strict-operator short-circuit applies: `null > 0` yields NULL, and the
-- predicate functions combine NULLs with the openCypher three-valued
-- logic (true trumps null in any(), false trumps null in all(), etc.).

-- agtype null in list: null > 0 = true in AGE, so any() = true
-- [null]: only null predicate, no true -> any() = NULL
SELECT * FROM cypher('predicate_functions', $$
RETURN any(x IN [null] WHERE x > 0)
$$) AS (result agtype);

-- agtype null + real values: all comparisons are true
-- one true (1 > 0) is enough: any() = true
SELECT * FROM cypher('predicate_functions', $$
RETURN any(x IN [null, 1, 2] WHERE x > 0)
$$) AS (result agtype);
Expand All @@ -144,34 +144,83 @@ SELECT * FROM cypher('predicate_functions', $$
RETURN all(x IN [1] WHERE null)
$$) AS (result agtype);

-- agtype null in list: null > 0 = true in AGE, so all() = true
-- no false, but one null -> all() = NULL
SELECT * FROM cypher('predicate_functions', $$
RETURN all(x IN [1, null, 2] WHERE x > 0)
$$) AS (result agtype);

-- -1 > 0 = false, so all() = false
-- -1 > 0 = false dominates the null -> all() = false
SELECT * FROM cypher('predicate_functions', $$
RETURN all(x IN [1, null, -1] WHERE x > 0)
$$) AS (result agtype);

-- agtype null > 0 = true in AGE, so none() = false
-- [null]: only null predicate, no true -> none() = NULL
SELECT * FROM cypher('predicate_functions', $$
RETURN none(x IN [null] WHERE x > 0)
$$) AS (result agtype);

-- 5 > 0 = true, so none() = false
-- one true (5 > 0) dominates: none() = false
SELECT * FROM cypher('predicate_functions', $$
RETURN none(x IN [null, 5] WHERE x > 0)
$$) AS (result agtype);

-- agtype null > 0 = true AND 5 > 0 = true: 2 matches, single = false
-- one definite true (5 > 0) and one null predicate: the null could also
-- be a match, so we cannot conclude exactly-one -> single() = NULL
SELECT * FROM cypher('predicate_functions', $$
RETURN single(x IN [null, 5] WHERE x > 0)
$$) AS (result agtype);

-- single() with null list: NULL (same as other predicate functions)
-- two definite trues dominate any null -> single() = false
SELECT * FROM cypher('predicate_functions', $$
RETURN single(x IN null WHERE x > 0)
RETURN single(x IN [null, 5, 6] WHERE x > 0)
$$) AS (result agtype);

-- only null predicates -> single() = NULL
SELECT * FROM cypher('predicate_functions', $$
RETURN single(x IN [null, null] WHERE x > 0)
$$) AS (result agtype);

-- one true + one false + one null: the null could be the second true
-- so we cannot conclude exactly-one -> single() = NULL
SELECT * FROM cypher('predicate_functions', $$
RETURN single(x IN [1, null, -1] WHERE x > 0)
$$) AS (result agtype);

--
-- Additional null/three-valued coverage for any()/all()/none()
--

-- any() with no true and one null -> NULL
SELECT * FROM cypher('predicate_functions', $$
RETURN any(x IN [null, -1] WHERE x > 0)
$$) AS (result agtype);

-- all() with one true and one null -> NULL (the null might be false)
SELECT * FROM cypher('predicate_functions', $$
RETURN all(x IN [1, null] WHERE x > 0)
$$) AS (result agtype);

-- all() with one definite false dominates any null -> false
SELECT * FROM cypher('predicate_functions', $$
RETURN all(x IN [null, -1] WHERE x > 0)
$$) AS (result agtype);

-- none() with no true and one null -> NULL
SELECT * FROM cypher('predicate_functions', $$
RETURN none(x IN [null, -1] WHERE x > 0)
$$) AS (result agtype);

-- IS NULL predicate now sees unwound null elements (issue #2393)
SELECT * FROM cypher('predicate_functions', $$
RETURN any(x IN [1, null] WHERE x IS NULL)
$$) AS (result agtype);

SELECT * FROM cypher('predicate_functions', $$
RETURN none(x IN [1, null] WHERE x IS NULL)
$$) AS (result agtype);

SELECT * FROM cypher('predicate_functions', $$
RETURN all(x IN [null, null] WHERE x IS NULL)
$$) AS (result agtype);

--
Expand Down
Loading
Loading