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
100 changes: 100 additions & 0 deletions regress/expected/cypher_match.out
Original file line number Diff line number Diff line change
Expand Up @@ -3533,6 +3533,106 @@ SELECT * FROM cypher('test_enable_containment', $$ EXPLAIN (costs off) MATCH (x:
Filter: ((agtype_access_operator(VARIADIC ARRAY[properties, '"school"'::agtype]) = '{"name": "XYZ College", "program": {"major": "Psyc", "degree": "BSc"}}'::agtype) AND (agtype_access_operator(VARIADIC ARRAY[properties, '"phone"'::agtype]) = '[123456789, 987654321, 456987123]'::agtype))
(2 rows)

--
-- issue 2308: MATCH after CREATE returns 0 rows
--
-- When all MATCH variables are already bound from a preceding CREATE + WITH,
-- the MATCH filter quals must evaluate after CREATE, not before.
--
SELECT create_graph('issue_2308');
NOTICE: graph "issue_2308" has been created
create_graph
--------------

(1 row)

-- Reporter's exact case: CREATE + WITH + MATCH + SET + RETURN
SELECT * FROM cypher('issue_2308', $$
CREATE (a:TestB3)-[e:B3REL]->(b:TestB3)
WITH a, e, b
MATCH p = (a)-[e]->(b)
SET a.something = 'something'
RETURN a
$$) AS (a agtype);
a
----------------------------------------------------------------------------------------------
{"id": 844424930131969, "label": "TestB3", "properties": {"something": "something"}}::vertex
(1 row)

-- Bound variables, no SET
SELECT * FROM cypher('issue_2308', $$
CREATE (a:T2)-[e:R2]->(b:T2)
WITH a, e, b
MATCH (a)-[e]->(b)
RETURN a, e, b
$$) AS (a agtype, e agtype, b agtype);
a | e | b
-------------------------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------+-------------------------------------------------------------------
{"id": 1407374883553281, "label": "T2", "properties": {}}::vertex | {"id": 1688849860263937, "label": "R2", "end_id": 1407374883553282, "start_id": 1407374883553281, "properties": {}}::edge | {"id": 1407374883553282, "label": "T2", "properties": {}}::vertex
(1 row)

-- Reversed direction: filter should reject (0 rows expected)
SELECT * FROM cypher('issue_2308', $$
CREATE (a:T3)-[e:R3]->(b:T3)
WITH a, e, b
MATCH (b)-[e]->(a)
RETURN a
$$) AS (a agtype);
a
---
(0 rows)

-- Node-only MATCH with bound variable
SELECT * FROM cypher('issue_2308', $$
CREATE (a:T4 {name: 'test'})
WITH a
MATCH (a)
RETURN a
$$) AS (a agtype);
a
---------------------------------------------------------------------------------
{"id": 2533274790395905, "label": "T4", "properties": {"name": "test"}}::vertex
(1 row)

-- MATCH after SET (SET is also DML, chain must be protected)
SELECT * FROM cypher('issue_2308', $$
CREATE (a:T5 {val: 1})-[e:R5]->(b:T5 {val: 2})
$$) AS (r agtype);
r
---
(0 rows)

SELECT * FROM cypher('issue_2308', $$
MATCH (a:T5)-[e:R5]->(b:T5)
SET a.val = 10
WITH a, e, b
MATCH (a)-[e]->(b)
RETURN a.val
$$) AS (val agtype);
val
-----
10
(1 row)

SELECT drop_graph('issue_2308', true);
NOTICE: drop cascades to 11 other objects
DETAIL: drop cascades to table issue_2308._ag_label_vertex
drop cascades to table issue_2308._ag_label_edge
drop cascades to table issue_2308."TestB3"
drop cascades to table issue_2308."B3REL"
drop cascades to table issue_2308."T2"
drop cascades to table issue_2308."R2"
drop cascades to table issue_2308."T3"
drop cascades to table issue_2308."R3"
drop cascades to table issue_2308."T4"
drop cascades to table issue_2308."T5"
drop cascades to table issue_2308."R5"
NOTICE: graph "issue_2308" has been dropped
drop_graph
------------

(1 row)

--
-- Clean up
--
Expand Down
55 changes: 55 additions & 0 deletions regress/sql/cypher_match.sql
Original file line number Diff line number Diff line change
Expand Up @@ -1437,6 +1437,61 @@ SELECT count(*) FROM cypher('test_enable_containment', $$ MATCH p=(x:Customer)-[
SELECT * FROM cypher('test_enable_containment', $$ EXPLAIN (costs off) MATCH (x:Customer)-[:bought ={store: 'Amazon', addr:{city: 'Vancouver', street: 30}}]->(y:Product) RETURN 0 $$) as (a agtype);
SELECT * FROM cypher('test_enable_containment', $$ EXPLAIN (costs off) MATCH (x:Customer ={school: { name: 'XYZ College',program: { major: 'Psyc', degree: 'BSc'} },phone: [ 123456789, 987654321, 456987123 ]}) RETURN 0 $$) as (a agtype);

--
-- issue 2308: MATCH after CREATE returns 0 rows
--
-- When all MATCH variables are already bound from a preceding CREATE + WITH,
-- the MATCH filter quals must evaluate after CREATE, not before.
--
SELECT create_graph('issue_2308');

-- Reporter's exact case: CREATE + WITH + MATCH + SET + RETURN
SELECT * FROM cypher('issue_2308', $$
CREATE (a:TestB3)-[e:B3REL]->(b:TestB3)
WITH a, e, b
MATCH p = (a)-[e]->(b)
SET a.something = 'something'
RETURN a
$$) AS (a agtype);

-- Bound variables, no SET
SELECT * FROM cypher('issue_2308', $$
CREATE (a:T2)-[e:R2]->(b:T2)
WITH a, e, b
MATCH (a)-[e]->(b)
RETURN a, e, b
$$) AS (a agtype, e agtype, b agtype);

-- Reversed direction: filter should reject (0 rows expected)
SELECT * FROM cypher('issue_2308', $$
CREATE (a:T3)-[e:R3]->(b:T3)
WITH a, e, b
MATCH (b)-[e]->(a)
RETURN a
$$) AS (a agtype);

-- Node-only MATCH with bound variable
SELECT * FROM cypher('issue_2308', $$
CREATE (a:T4 {name: 'test'})
WITH a
MATCH (a)
RETURN a
$$) AS (a agtype);

-- MATCH after SET (SET is also DML, chain must be protected)
SELECT * FROM cypher('issue_2308', $$
CREATE (a:T5 {val: 1})-[e:R5]->(b:T5 {val: 2})
$$) AS (r agtype);
SELECT * FROM cypher('issue_2308', $$
MATCH (a:T5)-[e:R5]->(b:T5)
SET a.val = 10
WITH a, e, b
MATCH (a)-[e]->(b)
RETURN a.val
$$) AS (val agtype);

SELECT drop_graph('issue_2308', true);

--
-- Clean up
--
Expand Down
38 changes: 38 additions & 0 deletions src/backend/parser/cypher_clause.c
Original file line number Diff line number Diff line change
Expand Up @@ -345,6 +345,7 @@ static bool isa_special_VLE_case(cypher_path *path);

static ParseNamespaceItem *find_pnsi(cypher_parsestate *cpstate, char *varname);
static bool has_list_comp_or_subquery(Node *expr, void *context);
static bool clause_chain_has_dml(cypher_clause *clause);

/*
* Add required permissions to the RTEPermissionInfo for a relation.
Expand Down Expand Up @@ -2917,6 +2918,21 @@ static Query *transform_cypher_match_pattern(cypher_parsestate *cpstate,

pnsi = transform_prev_cypher_clause(cpstate, clause->prev, true);
rte = pnsi->p_rte;

/*
* If the predecessor clause chain contains a data-modifying
* operation (CREATE, SET, DELETE, MERGE), mark the subquery
* RTE as a security barrier. This prevents PostgreSQL's
* optimizer from pushing MATCH filter quals down into the
* subquery, which would cause them to evaluate before the
* DML executes -- resulting in quals checking NULL values
* and filtering out all rows.
*/
if (clause_chain_has_dml(clause->prev))
{
rte->security_barrier = true;
}

rtindex = list_length(pstate->p_rtable);
/* rte is the first RangeTblEntry in pstate */
if (rtindex != 1)
Expand Down Expand Up @@ -6545,6 +6561,28 @@ static void advance_transform_entities_to_next_clause(List *entities)
}
}

/*
* Walk the clause chain and return true if any clause is a
* data-modifying operation (CREATE, SET, DELETE, or MERGE).
*/
static bool clause_chain_has_dml(cypher_clause *clause)
{
while (clause != NULL)
{
if (is_ag_node(clause->self, cypher_create) ||
is_ag_node(clause->self, cypher_set) ||
is_ag_node(clause->self, cypher_delete) ||
is_ag_node(clause->self, cypher_merge))
{
return true;
}

clause = clause->prev;
}

return false;
}

static Query *analyze_cypher_clause(transform_method transform,
cypher_clause *clause,
cypher_parsestate *parent_cpstate)
Expand Down