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
85 changes: 85 additions & 0 deletions regress/expected/cypher_match.out
Original file line number Diff line number Diff line change
Expand Up @@ -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
--
Expand Down
50 changes: 50 additions & 0 deletions regress/sql/cypher_match.sql
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Comment on lines 1504 to 1526
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The regression tests added for issue_2193 assert a specific row order (e.g., Jane then Tom; Berlin then Paris) but the Cypher queries don't include an ORDER BY. Without ordering, result order can vary with plan changes (seq scan order, optimizer choices), making the test output potentially flaky. Consider adding an ORDER BY on the returned expression (or changing assertions to use counts/sets) to make the expected output deterministic.

Copilot uses AI. Check for mistakes.

-- 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);

Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new label-deferral logic introduces a new branch for “DML predecessor + invalid label”. There’s no regression test covering a query that (1) has a DML predecessor and (2) references a non-existent label in MATCH while still returning a MATCH-introduced variable (e.g., CREATE ... WITH a MATCH (p:NoSuchLabel) RETURN p). Adding a test for this case would help catch planner/namespace regressions in the deferred-label path.

Suggested change
-- MATCH on non-existent label after DML predecessor still returns 0 rows
SELECT * FROM cypher('issue_2193', $$
CREATE (a:Person {name: 'Alice'})
WITH a
MATCH (p:NonExistentLabel)
RETURN p
$$) AS (result agtype);

Copilot uses AI. Check for mistakes.
-- 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
--
Expand Down
34 changes: 33 additions & 1 deletion src/backend/parser/cypher_clause.c
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down