Skip to content

Commit cd0aebc

Browse files
gregfeliceclaude
andcommitted
Support pattern expressions in WHERE clause via GLR parser (issue #1577)
Enable bare graph patterns as boolean expressions in WHERE clauses: MATCH (a:Person), (b:Person) WHERE (a)-[:KNOWS]->(b) -- now valid, equivalent to EXISTS(...) RETURN a.name, b.name Previously, this required wrapping in EXISTS(): WHERE EXISTS((a)-[:KNOWS]->(b)) The bare pattern syntax is standard openCypher and is used extensively in Neo4j. Its absence was the most frequently cited migration blocker. Implementation approach: - Switch the Cypher parser from LALR(1) to Bison GLR mode. GLR handles the inherent ambiguity between parenthesized expressions '(' expr ')' and graph path nodes '(' var_name label_opt props ')' by forking the parse stack and discarding the failing path. - Add anonymous_path as an expr_atom alternative with %dprec 1 (lower priority than expression path at %dprec 2). The action wraps the pattern in a cypher_sub_pattern + EXISTS SubLink, reusing the same transform_cypher_sub_pattern() machinery as explicit EXISTS(). - Extract make_exists_pattern_sublink() helper shared by both EXISTS(pattern) and bare pattern rules. - Fix YYLLOC_DEFAULT to use YYRHSLOC() for GLR compatibility. - %dprec annotations on expr_var/var_name_opt resolve the reduce/reduce conflict between expression variables and pattern node variables. Conflict budget: 7 shift/reduce (path extension vs arithmetic on -/<), 3 reduce/reduce (expr_var vs var_name_opt on )/}/=). All are expected and handled correctly by GLR forking + %dprec disambiguation. All 32 regression tests pass (31 existing + 1 new). New pattern_expression test covers: bare patterns, NOT patterns, labeled nodes, AND/OR combinations, left-directed patterns, anonymous nodes, multi-hop patterns, EXISTS() backward compatibility, and non-pattern expression regression checks. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 217467a commit cd0aebc

File tree

4 files changed

+446
-20
lines changed

4 files changed

+446
-20
lines changed

Makefile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@ REGRESS = scan \
111111
name_validation \
112112
jsonb_operators \
113113
list_comprehension \
114+
pattern_expression \
114115
map_projection \
115116
direct_field_access \
116117
security
Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,231 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
LOAD 'age';
20+
SET search_path TO ag_catalog;
21+
SELECT create_graph('pattern_expr');
22+
NOTICE: graph "pattern_expr" has been created
23+
create_graph
24+
--------------
25+
26+
(1 row)
27+
28+
--
29+
-- Setup test data
30+
--
31+
SELECT * FROM cypher('pattern_expr', $$
32+
CREATE (alice:Person {name: 'Alice'})-[:KNOWS]->(bob:Person {name: 'Bob'}),
33+
(alice)-[:WORKS_WITH]->(charlie:Person {name: 'Charlie'}),
34+
(dave:Person {name: 'Dave'})
35+
$$) AS (result agtype);
36+
result
37+
--------
38+
(0 rows)
39+
40+
--
41+
-- Basic pattern expression in WHERE
42+
--
43+
-- Bare pattern: (a)-[:REL]->(b)
44+
SELECT * FROM cypher('pattern_expr', $$
45+
MATCH (a:Person), (b:Person)
46+
WHERE (a)-[:KNOWS]->(b)
47+
RETURN a.name, b.name
48+
ORDER BY a.name, b.name
49+
$$) AS (a agtype, b agtype);
50+
a | b
51+
---------+-------
52+
"Alice" | "Bob"
53+
(1 row)
54+
55+
--
56+
-- NOT pattern expression
57+
--
58+
-- Find people who don't KNOW anyone
59+
SELECT * FROM cypher('pattern_expr', $$
60+
MATCH (a:Person)
61+
WHERE NOT (a)-[:KNOWS]->(:Person)
62+
RETURN a.name
63+
ORDER BY a.name
64+
$$) AS (result agtype);
65+
result
66+
-----------
67+
"Bob"
68+
"Charlie"
69+
"Dave"
70+
(3 rows)
71+
72+
--
73+
-- Pattern with labeled first node
74+
--
75+
SELECT * FROM cypher('pattern_expr', $$
76+
MATCH (a:Person), (b:Person)
77+
WHERE (a:Person)-[:KNOWS]->(b)
78+
RETURN a.name, b.name
79+
ORDER BY a.name
80+
$$) AS (a agtype, b agtype);
81+
a | b
82+
---------+-------
83+
"Alice" | "Bob"
84+
(1 row)
85+
86+
--
87+
-- Pattern combined with AND
88+
--
89+
SELECT * FROM cypher('pattern_expr', $$
90+
MATCH (a:Person), (b:Person)
91+
WHERE (a)-[:KNOWS]->(b) AND a.name = 'Alice'
92+
RETURN a.name, b.name
93+
$$) AS (a agtype, b agtype);
94+
a | b
95+
---------+-------
96+
"Alice" | "Bob"
97+
(1 row)
98+
99+
--
100+
-- Pattern combined with OR
101+
--
102+
SELECT * FROM cypher('pattern_expr', $$
103+
MATCH (a:Person), (b:Person)
104+
WHERE (a)-[:KNOWS]->(b) OR (a)-[:WORKS_WITH]->(b)
105+
RETURN a.name, b.name
106+
ORDER BY a.name, b.name
107+
$$) AS (a agtype, b agtype);
108+
a | b
109+
---------+-----------
110+
"Alice" | "Bob"
111+
"Alice" | "Charlie"
112+
(2 rows)
113+
114+
--
115+
-- Left-directed pattern
116+
--
117+
SELECT * FROM cypher('pattern_expr', $$
118+
MATCH (a:Person), (b:Person)
119+
WHERE (a)<-[:KNOWS]-(b)
120+
RETURN a.name, b.name
121+
ORDER BY a.name
122+
$$) AS (a agtype, b agtype);
123+
a | b
124+
-------+---------
125+
"Bob" | "Alice"
126+
(1 row)
127+
128+
--
129+
-- Pattern with anonymous nodes
130+
--
131+
-- Find anyone who has any outgoing KNOWS relationship
132+
SELECT * FROM cypher('pattern_expr', $$
133+
MATCH (a:Person)
134+
WHERE (a)-[:KNOWS]->()
135+
RETURN a.name
136+
ORDER BY a.name
137+
$$) AS (result agtype);
138+
result
139+
---------
140+
"Alice"
141+
(1 row)
142+
143+
--
144+
-- Multiple relationship pattern
145+
--
146+
SELECT * FROM cypher('pattern_expr', $$
147+
MATCH (a:Person), (c:Person)
148+
WHERE (a)-[:KNOWS]->()-[:WORKS_WITH]->(c)
149+
RETURN a.name, c.name
150+
ORDER BY a.name
151+
$$) AS (a agtype, c agtype);
152+
a | c
153+
---+---
154+
(0 rows)
155+
156+
--
157+
-- Existing EXISTS() syntax still works (backward compatibility)
158+
--
159+
SELECT * FROM cypher('pattern_expr', $$
160+
MATCH (a:Person), (b:Person)
161+
WHERE EXISTS((a)-[:KNOWS]->(b))
162+
RETURN a.name, b.name
163+
ORDER BY a.name
164+
$$) AS (a agtype, b agtype);
165+
a | b
166+
---------+-------
167+
"Alice" | "Bob"
168+
(1 row)
169+
170+
--
171+
-- Pattern expression produces same results as EXISTS()
172+
--
173+
SELECT * FROM cypher('pattern_expr', $$
174+
MATCH (a:Person)
175+
WHERE (a)-[:KNOWS]->(:Person)
176+
RETURN a.name
177+
ORDER BY a.name
178+
$$) AS (result agtype);
179+
result
180+
---------
181+
"Alice"
182+
(1 row)
183+
184+
SELECT * FROM cypher('pattern_expr', $$
185+
MATCH (a:Person)
186+
WHERE EXISTS((a)-[:KNOWS]->(:Person))
187+
RETURN a.name
188+
ORDER BY a.name
189+
$$) AS (result agtype);
190+
result
191+
---------
192+
"Alice"
193+
(1 row)
194+
195+
--
196+
-- Regular expressions still work (no regression)
197+
--
198+
SELECT * FROM cypher('pattern_expr', $$
199+
RETURN (1 + 2)
200+
$$) AS (result agtype);
201+
result
202+
--------
203+
3
204+
(1 row)
205+
206+
SELECT * FROM cypher('pattern_expr', $$
207+
MATCH (n:Person)
208+
WHERE n.name = 'Alice'
209+
RETURN (n.name)
210+
$$) AS (result agtype);
211+
result
212+
---------
213+
"Alice"
214+
(1 row)
215+
216+
--
217+
-- Cleanup
218+
--
219+
SELECT * FROM drop_graph('pattern_expr', true);
220+
NOTICE: drop cascades to 5 other objects
221+
DETAIL: drop cascades to table pattern_expr._ag_label_vertex
222+
drop cascades to table pattern_expr._ag_label_edge
223+
drop cascades to table pattern_expr."Person"
224+
drop cascades to table pattern_expr."KNOWS"
225+
drop cascades to table pattern_expr."WORKS_WITH"
226+
NOTICE: graph "pattern_expr" has been dropped
227+
drop_graph
228+
------------
229+
230+
(1 row)
231+

regress/sql/pattern_expression.sql

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
LOAD 'age';
21+
SET search_path TO ag_catalog;
22+
23+
SELECT create_graph('pattern_expr');
24+
25+
--
26+
-- Setup test data
27+
--
28+
SELECT * FROM cypher('pattern_expr', $$
29+
CREATE (alice:Person {name: 'Alice'})-[:KNOWS]->(bob:Person {name: 'Bob'}),
30+
(alice)-[:WORKS_WITH]->(charlie:Person {name: 'Charlie'}),
31+
(dave:Person {name: 'Dave'})
32+
$$) AS (result agtype);
33+
34+
--
35+
-- Basic pattern expression in WHERE
36+
--
37+
-- Bare pattern: (a)-[:REL]->(b)
38+
SELECT * FROM cypher('pattern_expr', $$
39+
MATCH (a:Person), (b:Person)
40+
WHERE (a)-[:KNOWS]->(b)
41+
RETURN a.name, b.name
42+
ORDER BY a.name, b.name
43+
$$) AS (a agtype, b agtype);
44+
45+
--
46+
-- NOT pattern expression
47+
--
48+
-- Find people who don't KNOW anyone
49+
SELECT * FROM cypher('pattern_expr', $$
50+
MATCH (a:Person)
51+
WHERE NOT (a)-[:KNOWS]->(:Person)
52+
RETURN a.name
53+
ORDER BY a.name
54+
$$) AS (result agtype);
55+
56+
--
57+
-- Pattern with labeled first node
58+
--
59+
SELECT * FROM cypher('pattern_expr', $$
60+
MATCH (a:Person), (b:Person)
61+
WHERE (a:Person)-[:KNOWS]->(b)
62+
RETURN a.name, b.name
63+
ORDER BY a.name
64+
$$) AS (a agtype, b agtype);
65+
66+
--
67+
-- Pattern combined with AND
68+
--
69+
SELECT * FROM cypher('pattern_expr', $$
70+
MATCH (a:Person), (b:Person)
71+
WHERE (a)-[:KNOWS]->(b) AND a.name = 'Alice'
72+
RETURN a.name, b.name
73+
$$) AS (a agtype, b agtype);
74+
75+
--
76+
-- Pattern combined with OR
77+
--
78+
SELECT * FROM cypher('pattern_expr', $$
79+
MATCH (a:Person), (b:Person)
80+
WHERE (a)-[:KNOWS]->(b) OR (a)-[:WORKS_WITH]->(b)
81+
RETURN a.name, b.name
82+
ORDER BY a.name, b.name
83+
$$) AS (a agtype, b agtype);
84+
85+
--
86+
-- Left-directed pattern
87+
--
88+
SELECT * FROM cypher('pattern_expr', $$
89+
MATCH (a:Person), (b:Person)
90+
WHERE (a)<-[:KNOWS]-(b)
91+
RETURN a.name, b.name
92+
ORDER BY a.name
93+
$$) AS (a agtype, b agtype);
94+
95+
--
96+
-- Pattern with anonymous nodes
97+
--
98+
-- Find anyone who has any outgoing KNOWS relationship
99+
SELECT * FROM cypher('pattern_expr', $$
100+
MATCH (a:Person)
101+
WHERE (a)-[:KNOWS]->()
102+
RETURN a.name
103+
ORDER BY a.name
104+
$$) AS (result agtype);
105+
106+
--
107+
-- Multiple relationship pattern
108+
--
109+
SELECT * FROM cypher('pattern_expr', $$
110+
MATCH (a:Person), (c:Person)
111+
WHERE (a)-[:KNOWS]->()-[:WORKS_WITH]->(c)
112+
RETURN a.name, c.name
113+
ORDER BY a.name
114+
$$) AS (a agtype, c agtype);
115+
116+
--
117+
-- Existing EXISTS() syntax still works (backward compatibility)
118+
--
119+
SELECT * FROM cypher('pattern_expr', $$
120+
MATCH (a:Person), (b:Person)
121+
WHERE EXISTS((a)-[:KNOWS]->(b))
122+
RETURN a.name, b.name
123+
ORDER BY a.name
124+
$$) AS (a agtype, b agtype);
125+
126+
--
127+
-- Pattern expression produces same results as EXISTS()
128+
--
129+
SELECT * FROM cypher('pattern_expr', $$
130+
MATCH (a:Person)
131+
WHERE (a)-[:KNOWS]->(:Person)
132+
RETURN a.name
133+
ORDER BY a.name
134+
$$) AS (result agtype);
135+
136+
SELECT * FROM cypher('pattern_expr', $$
137+
MATCH (a:Person)
138+
WHERE EXISTS((a)-[:KNOWS]->(:Person))
139+
RETURN a.name
140+
ORDER BY a.name
141+
$$) AS (result agtype);
142+
143+
--
144+
-- Regular expressions still work (no regression)
145+
--
146+
SELECT * FROM cypher('pattern_expr', $$
147+
RETURN (1 + 2)
148+
$$) AS (result agtype);
149+
150+
SELECT * FROM cypher('pattern_expr', $$
151+
MATCH (n:Person)
152+
WHERE n.name = 'Alice'
153+
RETURN (n.name)
154+
$$) AS (result agtype);
155+
156+
--
157+
-- Cleanup
158+
--
159+
SELECT * FROM drop_graph('pattern_expr', true);

0 commit comments

Comments
 (0)