Skip to content

Commit 44fb476

Browse files
committed
Allow safe Cypher reserved keywords in alias positions (#2355)
Cypher productions in `cypher_gram.y` that bind an alias via the AS keyword (RETURN/WITH/YIELD ... AS x and UNWIND ... AS x) only accepted plain identifiers. As a result, completely valid Cypher such as SELECT * FROM cypher('g', $$ RETURN 1 AS count $$) AS (a agtype); failed with `syntax error at or near "count"`, even though `count` is already accepted in other identifier positions (it appears in the existing `safe_keywords` list and is permitted in `func_name` and `schema_name`). This patch introduces a dedicated `var_name_alias` non-terminal used only in the three alias-binding sites (yield_item, return_item, unwind). It accepts everything `var_name` accepts, plus the entire `safe_keywords` set, so the 49 non-conflicting reserved keywords (count, exists, coalesce, match, return, where, order, limit, distinct, optional, detach, contains, starts, ends, in, is, not, ...) are now usable as aliases. The change is intentionally scoped to alias positions: * `var_name` itself (used by pattern-variable bindings like `(x:Label)`, edge bindings, named paths, and `expr_var` references) is unchanged. Allowing safe_keywords there triggers 156 shift/reduce conflicts because keyword tokens collide with their roles inside expressions and patterns. * `conflicted_keywords` (END, NULL, TRUE, FALSE) remain rejected in every position; they are genuinely ambiguous with literal/CASE productions. Regression coverage is added in `regress/sql/issue_2355.sql` / `regress/expected/issue_2355.out`, exercising the original repro, representative safe_keywords across RETURN/WITH/UNWIND, multiple keyword aliases in one projection, and explicit negative tests proving that END/NULL/TRUE/FALSE and pattern-position keywords still error out. Closes #2355.
1 parent 774e781 commit 44fb476

4 files changed

Lines changed: 358 additions & 5 deletions

File tree

Makefile

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,8 @@ REGRESS = scan \
177177
predicate_functions \
178178
map_projection \
179179
direct_field_access \
180-
security
180+
security \
181+
reserved_keyword_alias
181182

182183
ifneq ($(EXTRA_TESTS),)
183184
REGRESS += $(EXTRA_TESTS)
Lines changed: 237 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,237 @@
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+
/*
21+
* Regression coverage for issue #2355:
22+
* Cypher reserved keywords from the `safe_keywords` set must be accepted
23+
* in alias positions (RETURN/WITH/YIELD ... AS <name>, UNWIND ... AS <name>).
24+
* Conflicting tokens (END / NULL / TRUE / FALSE) must remain rejected.
25+
* Pattern variable bindings are intentionally still restricted to plain
26+
* identifiers.
27+
*/
28+
29+
LOAD 'age';
30+
SET search_path TO ag_catalog;
31+
32+
SELECT * FROM create_graph('issue_2355');
33+
NOTICE: graph "issue_2355" has been created
34+
create_graph
35+
--------------
36+
37+
(1 row)
38+
39+
40+
-- The exact reproducer from the issue (previously failed with
41+
-- "syntax error at or near "count"").
42+
SELECT * FROM cypher('issue_2355', $$ RETURN 1 AS count $$) AS (a agtype);
43+
a
44+
---
45+
1
46+
(1 row)
47+
48+
49+
-- Representative coverage across the safe_keywords set as RETURN aliases.
50+
SELECT * FROM cypher('issue_2355', $$ RETURN 1 AS exists $$) AS (a agtype);
51+
a
52+
---
53+
1
54+
(1 row)
55+
56+
SELECT * FROM cypher('issue_2355', $$ RETURN 1 AS coalesce $$) AS (a agtype);
57+
a
58+
---
59+
1
60+
(1 row)
61+
62+
SELECT * FROM cypher('issue_2355', $$ RETURN 1 AS match $$) AS (a agtype);
63+
a
64+
---
65+
1
66+
(1 row)
67+
68+
SELECT * FROM cypher('issue_2355', $$ RETURN 1 AS return $$) AS (a agtype);
69+
a
70+
---
71+
1
72+
(1 row)
73+
74+
SELECT * FROM cypher('issue_2355', $$ RETURN 1 AS where $$) AS (a agtype);
75+
a
76+
---
77+
1
78+
(1 row)
79+
80+
SELECT * FROM cypher('issue_2355', $$ RETURN 1 AS order $$) AS (a agtype);
81+
a
82+
---
83+
1
84+
(1 row)
85+
86+
SELECT * FROM cypher('issue_2355', $$ RETURN 1 AS limit $$) AS (a agtype);
87+
a
88+
---
89+
1
90+
(1 row)
91+
92+
SELECT * FROM cypher('issue_2355', $$ RETURN 1 AS distinct $$) AS (a agtype);
93+
a
94+
---
95+
1
96+
(1 row)
97+
98+
SELECT * FROM cypher('issue_2355', $$ RETURN 1 AS optional $$) AS (a agtype);
99+
a
100+
---
101+
1
102+
(1 row)
103+
104+
SELECT * FROM cypher('issue_2355', $$ RETURN 1 AS detach $$) AS (a agtype);
105+
a
106+
---
107+
1
108+
(1 row)
109+
110+
SELECT * FROM cypher('issue_2355', $$ RETURN 1 AS contains $$) AS (a agtype);
111+
a
112+
---
113+
1
114+
(1 row)
115+
116+
SELECT * FROM cypher('issue_2355', $$ RETURN 1 AS starts $$) AS (a agtype);
117+
a
118+
---
119+
1
120+
(1 row)
121+
122+
SELECT * FROM cypher('issue_2355', $$ RETURN 1 AS ends $$) AS (a agtype);
123+
a
124+
---
125+
1
126+
(1 row)
127+
128+
SELECT * FROM cypher('issue_2355', $$ RETURN 1 AS in $$) AS (a agtype);
129+
a
130+
---
131+
1
132+
(1 row)
133+
134+
SELECT * FROM cypher('issue_2355', $$ RETURN 1 AS is $$) AS (a agtype);
135+
a
136+
---
137+
1
138+
(1 row)
139+
140+
SELECT * FROM cypher('issue_2355', $$ RETURN 1 AS not $$) AS (a agtype);
141+
a
142+
---
143+
1
144+
(1 row)
145+
146+
SELECT * FROM cypher('issue_2355', $$ RETURN 1 AS yield $$) AS (a agtype);
147+
a
148+
---
149+
1
150+
(1 row)
151+
152+
SELECT * FROM cypher('issue_2355', $$ RETURN 1 AS call $$) AS (a agtype);
153+
a
154+
---
155+
1
156+
(1 row)
157+
158+
159+
-- Multiple keyword aliases in one projection.
160+
SELECT * FROM cypher('issue_2355',
161+
$$ RETURN 1 AS count, 2 AS exists, 3 AS where $$
162+
) AS (count agtype, ex agtype, w agtype);
163+
count | ex | w
164+
-------+----+---
165+
1 | 2 | 3
166+
(1 row)
167+
168+
169+
-- WITH ... AS <safe_keyword>: alias binding works.
170+
SELECT * FROM cypher('issue_2355',
171+
$$ WITH 1 AS count RETURN 1 AS x $$
172+
) AS (a agtype);
173+
a
174+
---
175+
1
176+
(1 row)
177+
178+
179+
-- UNWIND ... AS <safe_keyword>: alias binding works.
180+
SELECT * FROM cypher('issue_2355',
181+
$$ UNWIND [1, 2, 3] AS row RETURN 1 AS x $$
182+
) AS (a agtype);
183+
a
184+
---
185+
1
186+
1
187+
1
188+
(3 rows)
189+
190+
191+
-- conflicted_keywords (END, NULL, TRUE, FALSE) MUST still be rejected
192+
-- because they introduce real grammar ambiguity with literal/expression
193+
-- productions.
194+
SELECT * FROM cypher('issue_2355', $$ RETURN 1 AS null $$) AS (a agtype);
195+
ERROR: syntax error at or near "null"
196+
LINE 1: SELECT * FROM cypher('issue_2355', $$ RETURN 1 AS null $$) ...
197+
^
198+
SELECT * FROM cypher('issue_2355', $$ RETURN 1 AS true $$) AS (a agtype);
199+
ERROR: syntax error at or near "true"
200+
LINE 1: SELECT * FROM cypher('issue_2355', $$ RETURN 1 AS true $$) ...
201+
^
202+
SELECT * FROM cypher('issue_2355', $$ RETURN 1 AS false $$) AS (a agtype);
203+
ERROR: syntax error at or near "false"
204+
LINE 1: SELECT * FROM cypher('issue_2355', $$ RETURN 1 AS false $$) ...
205+
^
206+
SELECT * FROM cypher('issue_2355', $$ RETURN 1 AS end $$) AS (a agtype);
207+
ERROR: syntax error at or near "end"
208+
LINE 1: SELECT * FROM cypher('issue_2355', $$ RETURN 1 AS end $$) ...
209+
^
210+
211+
-- Pattern-variable positions are intentionally NOT broadened (would
212+
-- create shift/reduce conflicts). Confirm they still error.
213+
SELECT * FROM cypher('issue_2355',
214+
$$ MATCH (count) RETURN 1 AS x $$
215+
) AS (a agtype);
216+
ERROR: syntax error at or near "count"
217+
LINE 2: $$ MATCH (count) RETURN 1 AS x $$
218+
^
219+
220+
-- Plain identifiers naturally remain unaffected.
221+
SELECT * FROM cypher('issue_2355', $$ RETURN 1 AS my_alias $$) AS (a agtype);
222+
a
223+
---
224+
1
225+
(1 row)
226+
227+
228+
SELECT * FROM drop_graph('issue_2355', true);
229+
NOTICE: drop cascades to 2 other objects
230+
DETAIL: drop cascades to table issue_2355._ag_label_vertex
231+
drop cascades to table issue_2355._ag_label_edge
232+
NOTICE: graph "issue_2355" has been dropped
233+
drop_graph
234+
------------
235+
236+
(1 row)
237+
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
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+
/*
21+
* Regression coverage for issue #2355:
22+
* Cypher reserved keywords from the `safe_keywords` set must be accepted
23+
* in alias positions (RETURN/WITH/YIELD ... AS <name>, UNWIND ... AS <name>).
24+
* Conflicting tokens (END / NULL / TRUE / FALSE) must remain rejected.
25+
* Pattern variable bindings are intentionally still restricted to plain
26+
* identifiers.
27+
*/
28+
29+
LOAD 'age';
30+
SET search_path TO ag_catalog;
31+
32+
SELECT * FROM create_graph('issue_2355');
33+
34+
-- The exact reproducer from the issue (previously failed with
35+
-- "syntax error at or near "count"").
36+
SELECT * FROM cypher('issue_2355', $$ RETURN 1 AS count $$) AS (a agtype);
37+
38+
-- Representative coverage across the safe_keywords set as RETURN aliases.
39+
SELECT * FROM cypher('issue_2355', $$ RETURN 1 AS exists $$) AS (a agtype);
40+
SELECT * FROM cypher('issue_2355', $$ RETURN 1 AS coalesce $$) AS (a agtype);
41+
SELECT * FROM cypher('issue_2355', $$ RETURN 1 AS match $$) AS (a agtype);
42+
SELECT * FROM cypher('issue_2355', $$ RETURN 1 AS return $$) AS (a agtype);
43+
SELECT * FROM cypher('issue_2355', $$ RETURN 1 AS where $$) AS (a agtype);
44+
SELECT * FROM cypher('issue_2355', $$ RETURN 1 AS order $$) AS (a agtype);
45+
SELECT * FROM cypher('issue_2355', $$ RETURN 1 AS limit $$) AS (a agtype);
46+
SELECT * FROM cypher('issue_2355', $$ RETURN 1 AS distinct $$) AS (a agtype);
47+
SELECT * FROM cypher('issue_2355', $$ RETURN 1 AS optional $$) AS (a agtype);
48+
SELECT * FROM cypher('issue_2355', $$ RETURN 1 AS detach $$) AS (a agtype);
49+
SELECT * FROM cypher('issue_2355', $$ RETURN 1 AS contains $$) AS (a agtype);
50+
SELECT * FROM cypher('issue_2355', $$ RETURN 1 AS starts $$) AS (a agtype);
51+
SELECT * FROM cypher('issue_2355', $$ RETURN 1 AS ends $$) AS (a agtype);
52+
SELECT * FROM cypher('issue_2355', $$ RETURN 1 AS in $$) AS (a agtype);
53+
SELECT * FROM cypher('issue_2355', $$ RETURN 1 AS is $$) AS (a agtype);
54+
SELECT * FROM cypher('issue_2355', $$ RETURN 1 AS not $$) AS (a agtype);
55+
SELECT * FROM cypher('issue_2355', $$ RETURN 1 AS yield $$) AS (a agtype);
56+
SELECT * FROM cypher('issue_2355', $$ RETURN 1 AS call $$) AS (a agtype);
57+
58+
-- Multiple keyword aliases in one projection.
59+
SELECT * FROM cypher('issue_2355',
60+
$$ RETURN 1 AS count, 2 AS exists, 3 AS where $$
61+
) AS (count agtype, ex agtype, w agtype);
62+
63+
-- WITH ... AS <safe_keyword>: alias binding works.
64+
SELECT * FROM cypher('issue_2355',
65+
$$ WITH 1 AS count RETURN 1 AS x $$
66+
) AS (a agtype);
67+
68+
-- UNWIND ... AS <safe_keyword>: alias binding works.
69+
SELECT * FROM cypher('issue_2355',
70+
$$ UNWIND [1, 2, 3] AS row RETURN 1 AS x $$
71+
) AS (a agtype);
72+
73+
-- conflicted_keywords (END, NULL, TRUE, FALSE) MUST still be rejected
74+
-- because they introduce real grammar ambiguity with literal/expression
75+
-- productions.
76+
SELECT * FROM cypher('issue_2355', $$ RETURN 1 AS null $$) AS (a agtype);
77+
SELECT * FROM cypher('issue_2355', $$ RETURN 1 AS true $$) AS (a agtype);
78+
SELECT * FROM cypher('issue_2355', $$ RETURN 1 AS false $$) AS (a agtype);
79+
SELECT * FROM cypher('issue_2355', $$ RETURN 1 AS end $$) AS (a agtype);
80+
81+
-- Pattern-variable positions are intentionally NOT broadened (would
82+
-- create shift/reduce conflicts). Confirm they still error.
83+
SELECT * FROM cypher('issue_2355',
84+
$$ MATCH (count) RETURN 1 AS x $$
85+
) AS (a agtype);
86+
87+
-- Plain identifiers naturally remain unaffected.
88+
SELECT * FROM cypher('issue_2355', $$ RETURN 1 AS my_alias $$) AS (a agtype);
89+
90+
SELECT * FROM drop_graph('issue_2355', true);

0 commit comments

Comments
 (0)