Skip to content

Commit f686e93

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`; `schema_name` accepts the broader `reserved_keyword` set). 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. Reading a keyword-named alias back through `expr_var` still fails (e.g. `WITH 1 AS count RETURN count`) because `expr_var` reads through `var_name`. That asymmetry is captured as a known limitation in the regression suite and tracked separately in #2416. Regression coverage lives in `regress/sql/reserved_keyword_alias.sql` and `regress/expected/reserved_keyword_alias.out`, exercising: * the original repro, * representative safe_keywords across RETURN/WITH/UNWIND, * multiple keyword aliases in one projection, * a backtick-quoted alias positive case, * the known read-back limitation as a negative test, and * explicit negatives proving END/NULL/TRUE/FALSE and pattern-position keywords still error out. Closes #2355.
1 parent 774e781 commit f686e93

4 files changed

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

0 commit comments

Comments
 (0)