diff --git a/regress/expected/cypher_match.out b/regress/expected/cypher_match.out index ff2825ae0..157754114 100644 --- a/regress/expected/cypher_match.out +++ b/regress/expected/cypher_match.out @@ -3633,6 +3633,91 @@ NOTICE: graph "issue_2308" has been dropped (1 row) +-- +-- Issue 2193: CREATE ... WITH ... MATCH on brand-new label returns 0 rows +-- on first execution because match_check_valid_label() runs before +-- transform_prev_cypher_clause() creates the label table. +-- +SELECT create_graph('issue_2193'); +NOTICE: graph "issue_2193" has been created + create_graph +-------------- + +(1 row) + +-- Reporter's exact case: CREATE two Person nodes, then MATCH on Person +-- Should return 2 rows on the very first execution +SELECT * FROM cypher('issue_2193', $$ + CREATE (a:Person {name: 'Jane', livesIn: 'London'}), + (b:Person {name: 'Tom', livesIn: 'Copenhagen'}) + WITH a, b + MATCH (p:Person) + RETURN p.name ORDER BY p.name +$$) AS (result agtype); + result +-------- + "Jane" + "Tom" +(2 rows) + +-- Single CREATE + MATCH on brand-new label +SELECT * FROM cypher('issue_2193', $$ + CREATE (a:City {name: 'Berlin'}) + WITH a + MATCH (c:City) + RETURN c.name +$$) AS (result agtype); + result +---------- + "Berlin" +(1 row) + +-- MATCH on a label that now exists (second execution) still works +SELECT * FROM cypher('issue_2193', $$ + CREATE (a:City {name: 'Paris'}) + WITH a + MATCH (c:City) + RETURN c.name ORDER BY c.name +$$) AS (result agtype); + result +---------- + "Berlin" + "Paris" +(2 rows) + +-- MATCH on non-existent label without DML predecessor still returns 0 rows +SELECT * FROM cypher('issue_2193', $$ + MATCH (x:NonExistentLabel) + RETURN x +$$) AS (result agtype); + result +-------- +(0 rows) + +-- MATCH on non-existent label after DML predecessor still returns 0 rows +-- and MATCH-introduced variable (p) is properly registered +SELECT * FROM cypher('issue_2193', $$ + CREATE (a:Person {name: 'Alice'}) + WITH a + MATCH (p:NonExistentLabel) + RETURN p +$$) AS (result agtype); + result +-------- +(0 rows) + +SELECT drop_graph('issue_2193', true); +NOTICE: drop cascades to 4 other objects +DETAIL: drop cascades to table issue_2193._ag_label_vertex +drop cascades to table issue_2193._ag_label_edge +drop cascades to table issue_2193."Person" +drop cascades to table issue_2193."City" +NOTICE: graph "issue_2193" has been dropped + drop_graph +------------ + +(1 row) + -- -- Clean up -- diff --git a/regress/sql/cypher_match.sql b/regress/sql/cypher_match.sql index ebcd67b84..78117b73d 100644 --- a/regress/sql/cypher_match.sql +++ b/regress/sql/cypher_match.sql @@ -1492,6 +1492,56 @@ $$) AS (val agtype); SELECT drop_graph('issue_2308', true); +-- +-- Issue 2193: CREATE ... WITH ... MATCH on brand-new label returns 0 rows +-- on first execution because match_check_valid_label() runs before +-- transform_prev_cypher_clause() creates the label table. +-- +SELECT create_graph('issue_2193'); + +-- Reporter's exact case: CREATE two Person nodes, then MATCH on Person +-- Should return 2 rows on the very first execution +SELECT * FROM cypher('issue_2193', $$ + CREATE (a:Person {name: 'Jane', livesIn: 'London'}), + (b:Person {name: 'Tom', livesIn: 'Copenhagen'}) + WITH a, b + MATCH (p:Person) + RETURN p.name ORDER BY p.name +$$) AS (result agtype); + +-- Single CREATE + MATCH on brand-new label +SELECT * FROM cypher('issue_2193', $$ + CREATE (a:City {name: 'Berlin'}) + WITH a + MATCH (c:City) + RETURN c.name +$$) AS (result agtype); + +-- MATCH on a label that now exists (second execution) still works +SELECT * FROM cypher('issue_2193', $$ + CREATE (a:City {name: 'Paris'}) + WITH a + MATCH (c:City) + RETURN c.name ORDER BY c.name +$$) AS (result agtype); + +-- MATCH on non-existent label without DML predecessor still returns 0 rows +SELECT * FROM cypher('issue_2193', $$ + MATCH (x:NonExistentLabel) + RETURN x +$$) AS (result agtype); + +-- MATCH on non-existent label after DML predecessor still returns 0 rows +-- and MATCH-introduced variable (p) is properly registered +SELECT * FROM cypher('issue_2193', $$ + CREATE (a:Person {name: 'Alice'}) + WITH a + MATCH (p:NonExistentLabel) + RETURN p +$$) AS (result agtype); + +SELECT drop_graph('issue_2193', true); + -- -- Clean up -- diff --git a/src/backend/parser/cypher_clause.c b/src/backend/parser/cypher_clause.c index 446e97b3f..f00b2bc53 100644 --- a/src/backend/parser/cypher_clause.c +++ b/src/backend/parser/cypher_clause.c @@ -2639,7 +2639,15 @@ static Query *transform_cypher_match(cypher_parsestate *cpstate, cypher_match *match_self = (cypher_match*) clause->self; Node *where = match_self->where; - if(!match_check_valid_label(match_self, cpstate)) + /* + * Check label validity early unless the predecessor clause chain + * contains a data-modifying operation (CREATE, SET, DELETE, MERGE). + * DML predecessors may create new labels that are not yet in the + * cache, so the check is deferred to after transform_prev_cypher_clause() + * for those cases. + */ + if (!clause_chain_has_dml(clause->prev) && + !match_check_valid_label(match_self, cpstate)) { cypher_bool_const *l = make_ag_node(cypher_bool_const); cypher_bool_const *r = make_ag_node(cypher_bool_const); @@ -2949,6 +2957,30 @@ static Query *transform_cypher_match_pattern(cypher_parsestate *cpstate, */ pnsi = get_namespace_item(pstate, rte); query->targetList = expandNSItemAttrs(pstate, pnsi, 0, true, -1); + + /* + * Now that the predecessor chain is fully transformed and + * any CREATE-generated labels exist in the cache, check + * whether the MATCH pattern references valid labels. This + * deferred check is only needed when the chain has DML, + * since labels created by CREATE are not in the cache at + * the time of the early check in transform_cypher_match(). + */ + if (clause_chain_has_dml(clause->prev) && + !match_check_valid_label(self, cpstate)) + { + cypher_bool_const *l = make_ag_node(cypher_bool_const); + cypher_bool_const *r = make_ag_node(cypher_bool_const); + + l->boolean = true; + l->location = -1; + r->boolean = false; + r->location = -1; + + where = (Node *)makeSimpleA_Expr(AEXPR_OP, "=", + (Node *)l, + (Node *)r, -1); + } } transform_match_pattern(cpstate, query, self->pattern, where);