Skip to content

Commit 1c64267

Browse files
committed
fix: strip backticks from map literal keys in Cypher queries
Fixed an issue where backticks in map literal keys were being included in the returned key names instead of being properly stripped. Applied the fix consistently across all map-related parsing contexts. Changes: - Made CypherASTBuilder.stripBackticks() static and package-private to allow access from CypherExpressionBuilder - Updated CypherExpressionBuilder.parseMapLiteralExpression() to strip backticks from map keys (RETURN clause map literals) - Updated CypherExpressionBuilder.parseMapProperties() to strip backticks from map keys (pattern properties) - Updated CypherASTBuilder.visitMap() to strip backticks from map keys (CREATE/MERGE clause map literals) - Updated CypherExpressionBuilder.parseMapProjection() to strip backticks from both explicit keys and property names (map projections) - Added comprehensive test cases in CypherMapBackticksTest to verify: - Single backticked key in RETURN (e.g., `@rid`) - Multiple backticked keys in RETURN - Escaped backticks within keys (e.g., `key``with``backticks`) - Backticked keys in CREATE clause - Backticked keys in map projections Example queries that now work correctly: - RETURN: collect({`@rid`: ID(c), text: c.text}) - CREATE: CREATE (n {`@special`: 'value'}) - Map projection: n{.name, `@id`: n.id} Before: keys were returned as "`@rid`" (with backticks) After: keys are returned as "@Rid" (without backticks) https://claude.ai/code/session_01ATpxvdUh9HNtuBW7x9NmLT
1 parent 0ffaf5b commit 1c64267

3 files changed

Lines changed: 189 additions & 6 deletions

File tree

engine/src/main/java/com/arcadedb/query/opencypher/parser/CypherASTBuilder.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1074,7 +1074,7 @@ public Map<String, Object> visitMap(final Cypher25Parser.MapContext ctx) {
10741074
final List<Cypher25Parser.ExpressionContext> values = ctx.expression();
10751075

10761076
for (int i = 0; i < keys.size() && i < values.size(); i++) {
1077-
final String key = keys.get(i).getText();
1077+
final String key = stripBackticks(keys.get(i).getText());
10781078
// Parse as Expression
10791079
final Expression expr = expressionBuilder.parseExpression(values.get(i));
10801080

@@ -1138,7 +1138,7 @@ private List<String> extractLabels(final Cypher25Parser.LabelExpressionContext c
11381138
* @param name the name potentially wrapped in backticks
11391139
* @return the name without backticks
11401140
*/
1141-
private String stripBackticks(final String name) {
1141+
static String stripBackticks(final String name) {
11421142
if (name == null || name.length() < 2) {
11431143
return name;
11441144
}

engine/src/main/java/com/arcadedb/query/opencypher/parser/CypherExpressionBuilder.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1117,7 +1117,7 @@ MapExpression parseMapLiteralExpression(final Cypher25Parser.MapContext ctx) {
11171117
final List<Cypher25Parser.ExpressionContext> values = ctx.expression();
11181118

11191119
for (int i = 0; i < keys.size() && i < values.size(); i++) {
1120-
final String key = keys.get(i).getText();
1120+
final String key = CypherASTBuilder.stripBackticks(keys.get(i).getText());
11211121
final Expression valueExpr = parseExpression(values.get(i));
11221122
entries.put(key, valueExpr);
11231123
}
@@ -1328,7 +1328,7 @@ private Map<String, Object> parseMapProperties(final Cypher25Parser.MapContext c
13281328
final List<Cypher25Parser.ExpressionContext> values = ctx.expression();
13291329

13301330
for (int i = 0; i < keys.size() && i < values.size(); i++) {
1331-
final String key = keys.get(i).getText();
1331+
final String key = CypherASTBuilder.stripBackticks(keys.get(i).getText());
13321332
final Expression expr = parseExpression(values.get(i));
13331333
if (expr instanceof LiteralExpression) {
13341334
map.put(key, ((LiteralExpression) expr).getValue());
@@ -1356,12 +1356,12 @@ MapProjectionExpression parseMapProjection(final Cypher25Parser.MapProjectionCon
13561356
for (final Cypher25Parser.MapProjectionElementContext elemCtx : ctx.mapProjectionElement()) {
13571357
if (elemCtx.propertyKeyName() != null && elemCtx.expression() != null) {
13581358
// key: expression
1359-
final String key = elemCtx.propertyKeyName().getText();
1359+
final String key = CypherASTBuilder.stripBackticks(elemCtx.propertyKeyName().getText());
13601360
final Expression expr = parseExpression(elemCtx.expression());
13611361
elements.add(new MapProjectionExpression.ProjectionElement(key, expr));
13621362
} else if (elemCtx.property() != null) {
13631363
// .propertyName
1364-
final String propName = elemCtx.property().propertyKeyName().getText();
1364+
final String propName = CypherASTBuilder.stripBackticks(elemCtx.property().propertyKeyName().getText());
13651365
elements.add(new MapProjectionExpression.ProjectionElement(propName));
13661366
} else if (elemCtx.variable() != null) {
13671367
// variable (include another variable's value)
Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
/*
2+
* Copyright © 2021-present Arcade Data Ltd (info@arcadedata.com)
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*
16+
* SPDX-FileCopyrightText: 2021-present Arcade Data Ltd (info@arcadedata.com)
17+
* SPDX-License-Identifier: Apache-2.0
18+
*/
19+
package com.arcadedb.query.opencypher;
20+
21+
import com.arcadedb.database.Database;
22+
import com.arcadedb.database.DatabaseFactory;
23+
import com.arcadedb.query.sql.executor.Result;
24+
import com.arcadedb.query.sql.executor.ResultSet;
25+
import org.junit.jupiter.api.AfterEach;
26+
import org.junit.jupiter.api.BeforeEach;
27+
import org.junit.jupiter.api.Test;
28+
29+
import java.util.List;
30+
import java.util.Map;
31+
32+
import static org.assertj.core.api.Assertions.assertThat;
33+
34+
/**
35+
* Test for map literals with backticked keys.
36+
* Ensures that backticks are properly stripped from map keys in RETURN clauses.
37+
*/
38+
public class CypherMapBackticksTest {
39+
private Database database;
40+
41+
@BeforeEach
42+
void setUp() {
43+
database = new DatabaseFactory("./target/databases/testcyphermapbackticks").create();
44+
database.getSchema().createVertexType("DOCUMENT");
45+
database.getSchema().createVertexType("CHUNK");
46+
database.getSchema().createEdgeType("HAS_CHUNK");
47+
}
48+
49+
@AfterEach
50+
void tearDown() {
51+
if (database != null) {
52+
database.drop();
53+
database = null;
54+
}
55+
}
56+
57+
@Test
58+
void testMapLiteralWithBacktickedKeys() {
59+
// Create test data
60+
database.transaction(() -> {
61+
database.command("opencypher",
62+
"CREATE (d:DOCUMENT {name: 'Doc1'})-[:HAS_CHUNK]->(c:CHUNK {text: 'Content', llm_flag: true})");
63+
});
64+
65+
// Query with map literal using backticked key (fixed direction to match CREATE)
66+
final ResultSet result = database.query("opencypher",
67+
"MATCH (d:DOCUMENT)-->(c:CHUNK) WHERE c.llm_flag = true " +
68+
"RETURN collect({`@rid`: ID(c), text: c.text}) AS chunks");
69+
70+
assertThat(result.hasNext()).isTrue();
71+
final Result row = result.next();
72+
assertThat(row.hasProperty("chunks")).isTrue();
73+
74+
final List<Map<String, Object>> chunks = (List<Map<String, Object>>) row.getProperty("chunks");
75+
assertThat(chunks).isNotEmpty();
76+
77+
final Map<String, Object> firstChunk = chunks.get(0);
78+
79+
// The key should be "@rid" without backticks
80+
assertThat(firstChunk).containsKey("@rid");
81+
assertThat(firstChunk).doesNotContainKey("`@rid`");
82+
assertThat(firstChunk).containsKey("text");
83+
assertThat(firstChunk.get("text")).isEqualTo("Content");
84+
}
85+
86+
@Test
87+
void testMapLiteralWithMultipleBacktickedKeys() {
88+
// Create test data
89+
database.transaction(() -> {
90+
database.command("opencypher",
91+
"CREATE (d:DOCUMENT {name: 'Doc1'})-[:HAS_CHUNK]->(c:CHUNK {text: 'Content'})");
92+
});
93+
94+
// Query with multiple backticked keys (fixed direction to match CREATE)
95+
final ResultSet result = database.query("opencypher",
96+
"MATCH (d:DOCUMENT)-->(c:CHUNK) " +
97+
"RETURN {`@rid`: ID(c), `@type`: 'chunk', normalKey: c.text} AS data");
98+
99+
assertThat(result.hasNext()).isTrue();
100+
final Result row = result.next();
101+
assertThat(row.hasProperty("data")).isTrue();
102+
103+
final Map<String, Object> data = (Map<String, Object>) row.getProperty("data");
104+
105+
// All keys should be without backticks
106+
assertThat(data).containsKey("@rid");
107+
assertThat(data).doesNotContainKey("`@rid`");
108+
assertThat(data).containsKey("@type");
109+
assertThat(data).doesNotContainKey("`@type`");
110+
assertThat(data).containsKey("normalKey");
111+
assertThat(data.get("@type")).isEqualTo("chunk");
112+
assertThat(data.get("normalKey")).isEqualTo("Content");
113+
}
114+
115+
@Test
116+
void testMapLiteralWithEscapedBackticks() {
117+
// Create test data
118+
database.transaction(() -> {
119+
database.command("opencypher",
120+
"CREATE (n:DOCUMENT {name: 'Test'})");
121+
});
122+
123+
// Query with escaped backticks (`` -> `) in key name
124+
final ResultSet result = database.query("opencypher",
125+
"MATCH (n:DOCUMENT) " +
126+
"RETURN {`key``with``backticks`: n.name} AS data");
127+
128+
assertThat(result.hasNext()).isTrue();
129+
final Result row = result.next();
130+
assertThat(row.hasProperty("data")).isTrue();
131+
132+
final Map<String, Object> data = (Map<String, Object>) row.getProperty("data");
133+
134+
// The key should have single backticks (`` escaped to `)
135+
assertThat(data).containsKey("key`with`backticks");
136+
assertThat(data.get("key`with`backticks")).isEqualTo("Test");
137+
}
138+
139+
@Test
140+
void testCreateWithBacktickedMapKeys() {
141+
// Test CREATE clause with backticked keys in map literal
142+
database.transaction(() -> {
143+
database.command("opencypher",
144+
"CREATE (n:DOCUMENT {`@special`: 'value1', normalKey: 'value2'})");
145+
});
146+
147+
// Verify the properties were stored with correct key names (without backticks)
148+
final ResultSet result = database.query("opencypher",
149+
"MATCH (n:DOCUMENT) RETURN n.`@special` AS special, n.normalKey AS normal");
150+
151+
assertThat(result.hasNext()).isTrue();
152+
final Result row = result.next();
153+
assertThat((Object) row.getProperty("special")).isEqualTo("value1");
154+
assertThat((Object) row.getProperty("normal")).isEqualTo("value2");
155+
}
156+
157+
@Test
158+
void testMapProjectionWithBacktickedKeys() {
159+
// Test map projection with backticked keys
160+
database.transaction(() -> {
161+
database.command("opencypher",
162+
"CREATE (n:DOCUMENT {name: 'Doc1', id: 123})");
163+
});
164+
165+
// Query with map projection using backticked key
166+
final ResultSet result = database.query("opencypher",
167+
"MATCH (n:DOCUMENT) " +
168+
"RETURN n{.name, `@id`: n.id} AS data");
169+
170+
assertThat(result.hasNext()).isTrue();
171+
final Result row = result.next();
172+
assertThat(row.hasProperty("data")).isTrue();
173+
174+
final Map<String, Object> data = (Map<String, Object>) row.getProperty("data");
175+
176+
// The keys should be without backticks
177+
assertThat(data).containsKey("name");
178+
assertThat(data).containsKey("@id");
179+
assertThat(data).doesNotContainKey("`@id`");
180+
assertThat(data.get("name")).isEqualTo("Doc1");
181+
assertThat(data.get("@id")).isEqualTo(123);
182+
}
183+
}

0 commit comments

Comments
 (0)