Skip to content

Commit 9c2c441

Browse files
committed
Fix single-node labeled pattern expressions not filtering by label (#2443)
A single-node labeled pattern used as a boolean expression -- e.g. `WHERE (a:Person)`, `WHERE EXISTS((a:Person))` -- was accepted but did not test the bound vertex's label. It desugars to an EXISTS sub-pattern, and make_path_join_quals() returned early for vertex-only patterns (list_length(entities) < 3), emitting no quals. With no edge to carry a correlation, the sub-pattern referenced nothing from the enclosing query, so the planner produced an uncorrelated one-time InitPlan that was trivially true whenever any vertex of that label existed -- the predicate matched every outer row. Emit an explicit label-id filter for a vertex-only pattern whose vertex carries a non-default label and whose variable is declared in an ancestor parse state (i.e. a correlated reference). make_qual() builds a name-based id reference that resolves to the outer variable, so the filter both correlates the sub-pattern to that variable and enforces the label. Freshly scanned, non-correlated vertices (no ancestor binding) are untouched, so plain MATCH (a:Person) and "does any X exist" EXISTS checks are unaffected. Add regression coverage in pattern_expression: WHERE (a:Person), WHERE NOT (a:Person), and EXISTS((a:Company)) against a graph with a non-Person vertex. All 41 regression tests pass.
1 parent 2bc8e95 commit 9c2c441

3 files changed

Lines changed: 154 additions & 15 deletions

File tree

regress/expected/pattern_expression.out

Lines changed: 63 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -320,12 +320,15 @@ $$) AS (result agtype);
320320
--
321321
-- Single-node pattern on an already-bound variable: (a:Label)
322322
--
323-
-- NOTE: this is an EXISTS existence check on the bound variable, NOT an
324-
-- openCypher label predicate. A matching label is therefore always true
325-
-- (the variable is already bound), and a *different* label is rejected by
326-
-- AGE's pre-existing "multiple labels for variable" restriction rather than
327-
-- evaluating to false. Both behaviours are captured here so any future change
328-
-- to single-node-pattern semantics is caught by this test.
323+
-- NOTE: as of #2443 a single-node labeled pattern is a correlated label
324+
-- predicate -- in WHERE / EXISTS it tests whether the bound vertex actually
325+
-- has the label (see the WHERE (a:Person) / EXISTS((a:Company)) cases in the
326+
-- #2443 section below). Here the variable is already bound to the SAME label,
327+
-- so the predicate is trivially true (the label matches). A *different* label
328+
-- on an already-bound variable is still rejected by AGE's pre-existing
329+
-- "multiple labels for variable" restriction rather than evaluating to false;
330+
-- that is an orthogonal limitation, captured here so any future change to
331+
-- single-node-pattern semantics is caught by this test.
329332
SELECT * FROM cypher('pattern_expr', $$
330333
MATCH (a:Person)
331334
RETURN a.name, (a:Person)
@@ -439,16 +442,69 @@ $$) AS (name agtype);
439442
"Alice"
440443
(1 row)
441444

445+
--
446+
-- Single-node labeled pattern as a boolean (#2443)
447+
--
448+
-- A bound vertex carrying a label, e.g. (a:Person), must test that vertex's
449+
-- label rather than be trivially true. Add a non-Person vertex so the filter
450+
-- is observable (every other vertex in this graph is a :Person).
451+
SELECT * FROM cypher('pattern_expr', $$
452+
CREATE (:Company {name: 'Acme'})
453+
$$) AS (result agtype);
454+
result
455+
--------
456+
(0 rows)
457+
458+
-- bare single-node label predicate in WHERE: only the :Person vertices
459+
SELECT * FROM cypher('pattern_expr', $$
460+
MATCH (a)
461+
WHERE (a:Person)
462+
RETURN a.name
463+
ORDER BY a.name
464+
$$) AS (name agtype);
465+
name
466+
-----------
467+
"Alice"
468+
"Bob"
469+
"Charlie"
470+
"Dave"
471+
(4 rows)
472+
473+
-- negated: only the non-Person vertex
474+
SELECT * FROM cypher('pattern_expr', $$
475+
MATCH (a)
476+
WHERE NOT (a:Person)
477+
RETURN a.name
478+
ORDER BY a.name
479+
$$) AS (name agtype);
480+
name
481+
--------
482+
"Acme"
483+
(1 row)
484+
485+
-- EXISTS() form of a single-node label predicate
486+
SELECT * FROM cypher('pattern_expr', $$
487+
MATCH (a)
488+
WHERE EXISTS((a:Company))
489+
RETURN a.name
490+
ORDER BY a.name
491+
$$) AS (name agtype);
492+
name
493+
--------
494+
"Acme"
495+
(1 row)
496+
442497
--
443498
-- Cleanup
444499
--
445500
SELECT * FROM drop_graph('pattern_expr', true);
446-
NOTICE: drop cascades to 5 other objects
501+
NOTICE: drop cascades to 6 other objects
447502
DETAIL: drop cascades to table pattern_expr._ag_label_vertex
448503
drop cascades to table pattern_expr._ag_label_edge
449504
drop cascades to table pattern_expr."Person"
450505
drop cascades to table pattern_expr."KNOWS"
451506
drop cascades to table pattern_expr."WORKS_WITH"
507+
drop cascades to table pattern_expr."Company"
452508
NOTICE: graph "pattern_expr" has been dropped
453509
drop_graph
454510
------------

regress/sql/pattern_expression.sql

Lines changed: 43 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -222,12 +222,15 @@ $$) AS (result agtype);
222222
--
223223
-- Single-node pattern on an already-bound variable: (a:Label)
224224
--
225-
-- NOTE: this is an EXISTS existence check on the bound variable, NOT an
226-
-- openCypher label predicate. A matching label is therefore always true
227-
-- (the variable is already bound), and a *different* label is rejected by
228-
-- AGE's pre-existing "multiple labels for variable" restriction rather than
229-
-- evaluating to false. Both behaviours are captured here so any future change
230-
-- to single-node-pattern semantics is caught by this test.
225+
-- NOTE: as of #2443 a single-node labeled pattern is a correlated label
226+
-- predicate -- in WHERE / EXISTS it tests whether the bound vertex actually
227+
-- has the label (see the WHERE (a:Person) / EXISTS((a:Company)) cases in the
228+
-- #2443 section below). Here the variable is already bound to the SAME label,
229+
-- so the predicate is trivially true (the label matches). A *different* label
230+
-- on an already-bound variable is still rejected by AGE's pre-existing
231+
-- "multiple labels for variable" restriction rather than evaluating to false;
232+
-- that is an orthogonal limitation, captured here so any future change to
233+
-- single-node-pattern semantics is caught by this test.
231234
SELECT * FROM cypher('pattern_expr', $$
232235
MATCH (a:Person)
233236
RETURN a.name, (a:Person)
@@ -299,6 +302,40 @@ SELECT * FROM cypher('pattern_expr', $$
299302
ORDER BY a.name
300303
$$) AS (name agtype);
301304

305+
--
306+
-- Single-node labeled pattern as a boolean (#2443)
307+
--
308+
-- A bound vertex carrying a label, e.g. (a:Person), must test that vertex's
309+
-- label rather than be trivially true. Add a non-Person vertex so the filter
310+
-- is observable (every other vertex in this graph is a :Person).
311+
SELECT * FROM cypher('pattern_expr', $$
312+
CREATE (:Company {name: 'Acme'})
313+
$$) AS (result agtype);
314+
315+
-- bare single-node label predicate in WHERE: only the :Person vertices
316+
SELECT * FROM cypher('pattern_expr', $$
317+
MATCH (a)
318+
WHERE (a:Person)
319+
RETURN a.name
320+
ORDER BY a.name
321+
$$) AS (name agtype);
322+
323+
-- negated: only the non-Person vertex
324+
SELECT * FROM cypher('pattern_expr', $$
325+
MATCH (a)
326+
WHERE NOT (a:Person)
327+
RETURN a.name
328+
ORDER BY a.name
329+
$$) AS (name agtype);
330+
331+
-- EXISTS() form of a single-node label predicate
332+
SELECT * FROM cypher('pattern_expr', $$
333+
MATCH (a)
334+
WHERE EXISTS((a:Company))
335+
RETURN a.name
336+
ORDER BY a.name
337+
$$) AS (name agtype);
338+
302339
--
303340
-- Cleanup
304341
--

src/backend/parser/cypher_clause.c

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5452,10 +5452,56 @@ static List *make_path_join_quals(cypher_parsestate *cpstate, List *entities)
54525452
List *quals = NIL;
54535453
List *join_quals;
54545454

5455-
/* for vertex only queries, there is no work to do */
5455+
/*
5456+
* Vertex-only patterns have no edges, so the edge-driven correlation and
5457+
* label-filter logic below never runs. That is correct for a freshly
5458+
* scanned vertex -- its label comes from its label-table scan. But a
5459+
* vertex that refers to a variable from an ENCLOSING query -- e.g. the
5460+
* (a:Person) in MATCH (a) WHERE (a:Person) / EXISTS((a:Person)) -- is not
5461+
* scanned from its label table here. Without an explicit filter such a
5462+
* sub-pattern is uncorrelated and trivially true (the label is never
5463+
* tested). If the vertex carries a non-default label and its variable
5464+
* exists in an ancestor parse state, emit a label-id filter: make_qual
5465+
* builds a name-based id reference that resolves to the outer variable,
5466+
* which both correlates the sub-pattern to it and enforces the label.
5467+
*/
54565468
if (list_length(entities) < 3)
54575469
{
5458-
return NIL;
5470+
cypher_parsestate *parent_cpstate =
5471+
(cypher_parsestate *) cpstate->pstate.parentParseState;
5472+
ListCell *vlc;
5473+
5474+
if (parent_cpstate != NULL)
5475+
{
5476+
foreach (vlc, entities)
5477+
{
5478+
transform_entity *ent = lfirst(vlc);
5479+
char *label;
5480+
char *name;
5481+
5482+
if (ent->type != ENT_VERTEX)
5483+
{
5484+
continue;
5485+
}
5486+
5487+
label = ent->entity.node->label;
5488+
name = ent->entity.node->name;
5489+
5490+
if (label != NULL && !IS_DEFAULT_LABEL_VERTEX(label) &&
5491+
name != NULL &&
5492+
find_variable(parent_cpstate, name) != NULL)
5493+
{
5494+
Node *id_field = make_qual(cpstate, ent, "id");
5495+
5496+
quals = lappend(quals,
5497+
filter_vertices_on_label_id(cpstate,
5498+
id_field,
5499+
label));
5500+
}
5501+
}
5502+
}
5503+
5504+
return quals;
54595505
}
54605506

54615507
lc = list_head(entities);

0 commit comments

Comments
 (0)