Skip to content
Merged
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
89 changes: 89 additions & 0 deletions regress/expected/cypher_vle.out
Original file line number Diff line number Diff line change
Expand Up @@ -1109,6 +1109,95 @@ NOTICE: graph "issue_1910" has been dropped

(1 row)

-- issue 2092: VLE with chained OPTIONAL MATCH and NULL handling
-- Previously, chained OPTIONAL MATCH with VLE would either segfault
-- (with WHERE IS NOT NULL) or error with "arguments cannot be NULL"
-- (without WHERE) instead of producing correct NULL-extended rows.
SELECT create_graph('issue_2092');
NOTICE: graph "issue_2092" has been created
create_graph
--------------

(1 row)

-- Set up a small graph where some OPTIONAL MATCH paths exist and some don't
SELECT * FROM cypher('issue_2092', $$
CREATE (a:Person {name: 'Alice'})
CREATE (b:Person {name: 'Bob'})
CREATE (c:City {name: 'NYC'})
CREATE (d:City {name: 'LA'})
CREATE (e:Place {name: 'Central Park'})
CREATE (a)-[:LIVES_IN]->(c)
CREATE (c)-[:HAS_PLACE]->(e)
CREATE (b)-[:LIVES_IN]->(d)
$$) AS (result agtype);
result
--------
(0 rows)

-- Alice lives in NYC which has Central Park.
-- Bob lives in LA which has no places.
-- VLE + chained OPTIONAL MATCH + WHERE IS NOT NULL: should return rows
-- without crashing (was: segfault)
SELECT * FROM cypher('issue_2092', $$
MATCH (p:Person)-[:LIVES_IN*]->(c:City)
OPTIONAL MATCH (c)-[:HAS_PLACE*]->(place)
OPTIONAL MATCH (place)-[:NEARBY*]->(other)
WHERE place IS NOT NULL
RETURN p.name, place.name, other
ORDER BY p.name
$$) AS (person agtype, place agtype, other agtype);
person | place | other
---------+----------------+-------
"Alice" | "Central Park" |
"Bob" | |
(2 rows)

-- VLE + chained OPTIONAL MATCH without WHERE: should return NULL-extended
-- rows without error (was: "match_vle_terminal_edge() arguments cannot be
-- NULL")
SELECT * FROM cypher('issue_2092', $$
MATCH (p:Person)-[:LIVES_IN*]->(c:City)
OPTIONAL MATCH (c)-[:HAS_PLACE*]->(place)
OPTIONAL MATCH (place)-[:NEARBY*]->(other)
RETURN p.name, place.name, other
ORDER BY p.name
$$) AS (person agtype, place agtype, other agtype);
person | place | other
---------+----------------+-------
"Alice" | "Central Park" |
"Bob" | |
(2 rows)

-- Verify the happy path still works: Alice's full chain resolves
SELECT * FROM cypher('issue_2092', $$
MATCH (p:Person)-[:LIVES_IN*]->(c:City)
OPTIONAL MATCH (c)-[:HAS_PLACE*]->(place)
WHERE place IS NOT NULL
RETURN p.name, c.name, place.name
ORDER BY p.name
$$) AS (person agtype, city agtype, place agtype);
person | city | place
---------+-------+----------------
"Alice" | "NYC" | "Central Park"
"Bob" | "LA" |
(2 rows)

SELECT drop_graph('issue_2092', true);
NOTICE: drop cascades to 7 other objects
DETAIL: drop cascades to table issue_2092._ag_label_vertex
drop cascades to table issue_2092._ag_label_edge
drop cascades to table issue_2092."Person"
drop cascades to table issue_2092."City"
drop cascades to table issue_2092."Place"
drop cascades to table issue_2092."LIVES_IN"
drop cascades to table issue_2092."HAS_PLACE"
NOTICE: graph "issue_2092" has been dropped
drop_graph
------------

(1 row)

--
-- Clean up
--
Expand Down
53 changes: 53 additions & 0 deletions regress/sql/cypher_vle.sql
Original file line number Diff line number Diff line change
Expand Up @@ -357,6 +357,59 @@ SELECT * FROM cypher('issue_1910', $$ MATCH (n) WHERE EXISTS((n)-[*2..2]-({name:

SELECT drop_graph('issue_1910', true);

-- issue 2092: VLE with chained OPTIONAL MATCH and NULL handling
-- Previously, chained OPTIONAL MATCH with VLE would either segfault
-- (with WHERE IS NOT NULL) or error with "arguments cannot be NULL"
-- (without WHERE) instead of producing correct NULL-extended rows.
SELECT create_graph('issue_2092');

-- Set up a small graph where some OPTIONAL MATCH paths exist and some don't
SELECT * FROM cypher('issue_2092', $$
CREATE (a:Person {name: 'Alice'})
CREATE (b:Person {name: 'Bob'})
CREATE (c:City {name: 'NYC'})
CREATE (d:City {name: 'LA'})
CREATE (e:Place {name: 'Central Park'})
CREATE (a)-[:LIVES_IN]->(c)
CREATE (c)-[:HAS_PLACE]->(e)
CREATE (b)-[:LIVES_IN]->(d)
$$) AS (result agtype);

-- Alice lives in NYC which has Central Park.
-- Bob lives in LA which has no places.
-- VLE + chained OPTIONAL MATCH + WHERE IS NOT NULL: should return rows
-- without crashing (was: segfault)
SELECT * FROM cypher('issue_2092', $$
MATCH (p:Person)-[:LIVES_IN*]->(c:City)
OPTIONAL MATCH (c)-[:HAS_PLACE*]->(place)
OPTIONAL MATCH (place)-[:NEARBY*]->(other)
WHERE place IS NOT NULL
RETURN p.name, place.name, other
ORDER BY p.name
$$) AS (person agtype, place agtype, other agtype);

-- VLE + chained OPTIONAL MATCH without WHERE: should return NULL-extended
-- rows without error (was: "match_vle_terminal_edge() arguments cannot be
-- NULL")
SELECT * FROM cypher('issue_2092', $$
MATCH (p:Person)-[:LIVES_IN*]->(c:City)
OPTIONAL MATCH (c)-[:HAS_PLACE*]->(place)
OPTIONAL MATCH (place)-[:NEARBY*]->(other)
RETURN p.name, place.name, other
ORDER BY p.name
$$) AS (person agtype, place agtype, other agtype);

-- Verify the happy path still works: Alice's full chain resolves
SELECT * FROM cypher('issue_2092', $$
MATCH (p:Person)-[:LIVES_IN*]->(c:City)
OPTIONAL MATCH (c)-[:HAS_PLACE*]->(place)
WHERE place IS NOT NULL
RETURN p.name, c.name, place.name
ORDER BY p.name
$$) AS (person agtype, city agtype, place agtype);

SELECT drop_graph('issue_2092', true);

--
-- Clean up
--
Expand Down
62 changes: 43 additions & 19 deletions src/backend/utils/adt/age_vle.c
Original file line number Diff line number Diff line change
Expand Up @@ -561,6 +561,11 @@ static VLE_local_context *build_local_vle_context(FunctionCallInfo fcinfo,
/* get and update the start vertex id */
if (PG_ARGISNULL(1) || is_agtype_null(AG_GET_ARG_AGTYPE_P(1)))
{
/* if there are no more vertices to process, return NULL */
if (vlelctx->next_vertex == NULL)
{
return NULL;
}
vlelctx->vsid = get_graphid(vlelctx->next_vertex);
/* increment to the next vertex */
vlelctx->next_vertex = next_GraphIdNode(vlelctx->next_vertex);
Expand Down Expand Up @@ -1733,6 +1738,16 @@ Datum age_vle(PG_FUNCTION_ARGS)
/* build the local vle context */
vlelctx = build_local_vle_context(fcinfo, funcctx);

/*
* If the context is NULL, there are no paths to find.
* This can happen when a cached VLE context has exhausted
* its vertex list (e.g., from a NULL OPTIONAL MATCH variable).
*/
if (vlelctx == NULL)
{
SRF_RETURN_DONE(funcctx);
}

/*
* Point the function call context's user pointer to the local VLE
* context just created
Expand Down Expand Up @@ -1934,6 +1949,16 @@ Datum age_match_two_vle_edges(PG_FUNCTION_ARGS)
graphid *left_array, *right_array;
int left_array_size;

/*
* If either argument is NULL, return FALSE. This can occur in
* OPTIONAL MATCH (LEFT JOIN) contexts where a preceding clause
* produced no results.
*/
if (PG_ARGISNULL(0) || PG_ARGISNULL(1))
{
PG_RETURN_BOOL(false);
}

/* get the VLE_path_container argument */
agt_arg_vpc = AG_GET_ARG_AGTYPE_P(0);

Expand Down Expand Up @@ -2008,12 +2033,14 @@ Datum age_match_vle_edge_to_id_qual(PG_FUNCTION_ARGS)
errmsg("age_match_vle_edge_to_id_qual() invalid number of arguments")));
}

/* the arguments cannot be NULL */
/*
* If any argument is NULL, return FALSE. This can occur in
* OPTIONAL MATCH (LEFT JOIN) contexts where a preceding clause
* produced no results.
*/
if (nulls[0] || nulls[1] || nulls[2])
{
ereport(ERROR,
(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
errmsg("age_match_vle_edge_to_id_qual() arguments must be non NULL")));
PG_RETURN_BOOL(false);
}

/* get the VLE_path_container argument */
Expand Down Expand Up @@ -2233,26 +2260,27 @@ Datum age_match_vle_terminal_edge(PG_FUNCTION_ARGS)
if (nargs != 3)
{
ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
errmsg("age_match_terminal_edge() invalid number of arguments")));
errmsg("age_match_vle_terminal_edge() invalid number of arguments")));
}

/* the arguments cannot be NULL */
/*
* If any argument is NULL, return FALSE. This can occur when this
* function is used as a join qual in an OPTIONAL MATCH (LEFT JOIN)
* where a preceding OPTIONAL MATCH produced no results. Returning
* FALSE allows PostgreSQL to produce the correct NULL-extended rows.
*/
if (nulls[0] || nulls[1] || nulls[2])
{
ereport(ERROR,
(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
errmsg("match_vle_terminal_edge() arguments cannot be NULL")));
PG_RETURN_BOOL(false);
}

/* get the vpc */
agt_arg_path = DATUM_GET_AGTYPE_P(args[2]);

/* it cannot be NULL */
/* if the vpc is an agtype NULL, return FALSE */
if (is_agtype_null(agt_arg_path))
{
ereport(ERROR,
(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
errmsg("match_vle_terminal_edge() argument 3 cannot be NULL")));
PG_RETURN_BOOL(false);
}

/*
Expand Down Expand Up @@ -2290,9 +2318,7 @@ Datum age_match_vle_terminal_edge(PG_FUNCTION_ARGS)
}
else
{
ereport(ERROR,
(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
errmsg("match_vle_terminal_edge() argument 1 must be non NULL")));
PG_RETURN_BOOL(false);
}
}
else if (types[0] == GRAPHIDOID)
Expand Down Expand Up @@ -2320,9 +2346,7 @@ Datum age_match_vle_terminal_edge(PG_FUNCTION_ARGS)
}
else
{
ereport(ERROR,
(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
errmsg("match_vle_terminal_edge() argument 2 must be non NULL")));
PG_RETURN_BOOL(false);
}
}
else if (types[1] == GRAPHIDOID)
Expand Down