This is an automated email from the ASF dual-hosted git repository.

jrgemignani pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/age.git


The following commit(s) were added to refs/heads/master by this push:
     new fe48740d Fix single-node labeled pattern expressions not filtering by 
label (#2443) (#2444)
fe48740d is described below

commit fe48740d8597710e54b195176de332f7e95910e1
Author: Greg Felice <[email protected]>
AuthorDate: Mon Jun 29 12:57:18 2026 -0400

    Fix single-node labeled pattern expressions not filtering by label (#2443) 
(#2444)
    
    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.
---
 regress/expected/pattern_expression.out | 70 +++++++++++++++++++++++++++++----
 regress/sql/pattern_expression.sql      | 49 ++++++++++++++++++++---
 src/backend/parser/cypher_clause.c      | 50 ++++++++++++++++++++++-
 3 files changed, 154 insertions(+), 15 deletions(-)

diff --git a/regress/expected/pattern_expression.out 
b/regress/expected/pattern_expression.out
index 0494d49b..93a02e3f 100644
--- a/regress/expected/pattern_expression.out
+++ b/regress/expected/pattern_expression.out
@@ -320,12 +320,15 @@ $$) AS (result agtype);
 --
 -- Single-node pattern on an already-bound variable: (a:Label)
 --
--- NOTE: this is an EXISTS existence check on the bound variable, NOT an
--- openCypher label predicate. A matching label is therefore always true
--- (the variable is already bound), and a *different* label is rejected by
--- AGE's pre-existing "multiple labels for variable" restriction rather than
--- evaluating to false. Both behaviours are captured here so any future change
--- to single-node-pattern semantics is caught by this test.
+-- NOTE: as of #2443 a single-node labeled pattern is a correlated label
+-- predicate -- in WHERE / EXISTS it tests whether the bound vertex actually
+-- has the label (see the WHERE (a:Person) / EXISTS((a:Company)) cases in the
+-- #2443 section below). Here the variable is already bound to the SAME label,
+-- so the predicate is trivially true (the label matches). A *different* label
+-- on an already-bound variable is still rejected by AGE's pre-existing
+-- "multiple labels for variable" restriction rather than evaluating to false;
+-- that is an orthogonal limitation, captured here so any future change to
+-- single-node-pattern semantics is caught by this test.
 SELECT * FROM cypher('pattern_expr', $$
     MATCH (a:Person)
     RETURN a.name, (a:Person)
@@ -439,16 +442,69 @@ $$) AS (name agtype);
  "Alice"
 (1 row)
 
+--
+-- Single-node labeled pattern as a boolean (#2443)
+--
+-- A bound vertex carrying a label, e.g. (a:Person), must test that vertex's
+-- label rather than be trivially true. Add a non-Person vertex so the filter
+-- is observable (every other vertex in this graph is a :Person).
+SELECT * FROM cypher('pattern_expr', $$
+    CREATE (:Company {name: 'Acme'})
+$$) AS (result agtype);
+ result 
+--------
+(0 rows)
+
+-- bare single-node label predicate in WHERE: only the :Person vertices
+SELECT * FROM cypher('pattern_expr', $$
+    MATCH (a)
+    WHERE (a:Person)
+    RETURN a.name
+    ORDER BY a.name
+$$) AS (name agtype);
+   name    
+-----------
+ "Alice"
+ "Bob"
+ "Charlie"
+ "Dave"
+(4 rows)
+
+-- negated: only the non-Person vertex
+SELECT * FROM cypher('pattern_expr', $$
+    MATCH (a)
+    WHERE NOT (a:Person)
+    RETURN a.name
+    ORDER BY a.name
+$$) AS (name agtype);
+  name  
+--------
+ "Acme"
+(1 row)
+
+-- EXISTS() form of a single-node label predicate
+SELECT * FROM cypher('pattern_expr', $$
+    MATCH (a)
+    WHERE EXISTS((a:Company))
+    RETURN a.name
+    ORDER BY a.name
+$$) AS (name agtype);
+  name  
+--------
+ "Acme"
+(1 row)
+
 --
 -- Cleanup
 --
 SELECT * FROM drop_graph('pattern_expr', true);
-NOTICE:  drop cascades to 5 other objects
+NOTICE:  drop cascades to 6 other objects
 DETAIL:  drop cascades to table pattern_expr._ag_label_vertex
 drop cascades to table pattern_expr._ag_label_edge
 drop cascades to table pattern_expr."Person"
 drop cascades to table pattern_expr."KNOWS"
 drop cascades to table pattern_expr."WORKS_WITH"
+drop cascades to table pattern_expr."Company"
 NOTICE:  graph "pattern_expr" has been dropped
  drop_graph 
 ------------
diff --git a/regress/sql/pattern_expression.sql 
b/regress/sql/pattern_expression.sql
index fff8476e..9ded819e 100644
--- a/regress/sql/pattern_expression.sql
+++ b/regress/sql/pattern_expression.sql
@@ -222,12 +222,15 @@ $$) AS (result agtype);
 --
 -- Single-node pattern on an already-bound variable: (a:Label)
 --
--- NOTE: this is an EXISTS existence check on the bound variable, NOT an
--- openCypher label predicate. A matching label is therefore always true
--- (the variable is already bound), and a *different* label is rejected by
--- AGE's pre-existing "multiple labels for variable" restriction rather than
--- evaluating to false. Both behaviours are captured here so any future change
--- to single-node-pattern semantics is caught by this test.
+-- NOTE: as of #2443 a single-node labeled pattern is a correlated label
+-- predicate -- in WHERE / EXISTS it tests whether the bound vertex actually
+-- has the label (see the WHERE (a:Person) / EXISTS((a:Company)) cases in the
+-- #2443 section below). Here the variable is already bound to the SAME label,
+-- so the predicate is trivially true (the label matches). A *different* label
+-- on an already-bound variable is still rejected by AGE's pre-existing
+-- "multiple labels for variable" restriction rather than evaluating to false;
+-- that is an orthogonal limitation, captured here so any future change to
+-- single-node-pattern semantics is caught by this test.
 SELECT * FROM cypher('pattern_expr', $$
     MATCH (a:Person)
     RETURN a.name, (a:Person)
@@ -299,6 +302,40 @@ SELECT * FROM cypher('pattern_expr', $$
     ORDER BY a.name
 $$) AS (name agtype);
 
+--
+-- Single-node labeled pattern as a boolean (#2443)
+--
+-- A bound vertex carrying a label, e.g. (a:Person), must test that vertex's
+-- label rather than be trivially true. Add a non-Person vertex so the filter
+-- is observable (every other vertex in this graph is a :Person).
+SELECT * FROM cypher('pattern_expr', $$
+    CREATE (:Company {name: 'Acme'})
+$$) AS (result agtype);
+
+-- bare single-node label predicate in WHERE: only the :Person vertices
+SELECT * FROM cypher('pattern_expr', $$
+    MATCH (a)
+    WHERE (a:Person)
+    RETURN a.name
+    ORDER BY a.name
+$$) AS (name agtype);
+
+-- negated: only the non-Person vertex
+SELECT * FROM cypher('pattern_expr', $$
+    MATCH (a)
+    WHERE NOT (a:Person)
+    RETURN a.name
+    ORDER BY a.name
+$$) AS (name agtype);
+
+-- EXISTS() form of a single-node label predicate
+SELECT * FROM cypher('pattern_expr', $$
+    MATCH (a)
+    WHERE EXISTS((a:Company))
+    RETURN a.name
+    ORDER BY a.name
+$$) AS (name agtype);
+
 --
 -- Cleanup
 --
diff --git a/src/backend/parser/cypher_clause.c 
b/src/backend/parser/cypher_clause.c
index a3a1a504..6582ff8d 100644
--- a/src/backend/parser/cypher_clause.c
+++ b/src/backend/parser/cypher_clause.c
@@ -5873,10 +5873,56 @@ static List *make_path_join_quals(cypher_parsestate 
*cpstate, List *entities)
     List *quals = NIL;
     List *join_quals;
 
-    /* for vertex only queries, there is no work to do */
+    /*
+     * Vertex-only patterns have no edges, so the edge-driven correlation and
+     * label-filter logic below never runs. That is correct for a freshly
+     * scanned vertex -- its label comes from its label-table scan. But a
+     * vertex that refers to a variable from an ENCLOSING query -- e.g. the
+     * (a:Person) in MATCH (a) WHERE (a:Person) / EXISTS((a:Person)) -- is not
+     * scanned from its label table here. Without an explicit filter such a
+     * sub-pattern is uncorrelated and trivially true (the label is never
+     * tested). If the vertex carries a non-default label and its variable
+     * exists in an ancestor parse state, emit a label-id filter: make_qual
+     * builds a name-based id reference that resolves to the outer variable,
+     * which both correlates the sub-pattern to it and enforces the label.
+     */
     if (list_length(entities) < 3)
     {
-        return NIL;
+        cypher_parsestate *parent_cpstate =
+            (cypher_parsestate *) cpstate->pstate.parentParseState;
+        ListCell *vlc;
+
+        if (parent_cpstate != NULL)
+        {
+            foreach (vlc, entities)
+            {
+                transform_entity *ent = lfirst(vlc);
+                char *label;
+                char *name;
+
+                if (ent->type != ENT_VERTEX)
+                {
+                    continue;
+                }
+
+                label = ent->entity.node->label;
+                name = ent->entity.node->name;
+
+                if (label != NULL && !IS_DEFAULT_LABEL_VERTEX(label) &&
+                    name != NULL &&
+                    find_variable(parent_cpstate, name) != NULL)
+                {
+                    Node *id_field = make_qual(cpstate, ent, "id");
+
+                    quals = lappend(quals,
+                                    filter_vertices_on_label_id(cpstate,
+                                                                id_field,
+                                                                label));
+                }
+            }
+        }
+
+        return quals;
     }
 
     lc = list_head(entities);

Reply via email to