#3952 fix: Cypher -> EXISTS { MATCH (p)-[:WORKS_WITH]->() } returns false when relationship type embeds a Cypher keyword after underscore - #3962
Conversation
…hen relationship type name embeds a Cypher keyword after underscore Closes #3952 `matchesKeywordAt()` in ExistsExpression used `Character.isLetterOrDigit()` for word-boundary detection. Because underscore is not a letter or digit in Java, "WITH" inside "WORKS_WITH" was falsely matched as the Cypher WITH clause keyword, corrupting the injected subquery and causing a silently-caught parse exception that returned false. Fix: replace the boundary check with `isCypherIdentifierChar()` which also treats `_` as part of an identifier, consistent with Cypher identifier rules. Affects any relationship type whose name ends with a Cypher keyword after an underscore (e.g. _WITH, _WHERE, _RETURN). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Not up to standards ⛔🔴 Issues
|
| Category | Results |
|---|---|
| CodeStyle | 1 minor |
🟢 Coverage 90.91% diff coverage · -7.48% coverage variation
Metric Results Coverage variation ✅ -7.48% coverage variation Diff coverage ✅ 90.91% diff coverage Coverage variation details
Coverable lines Covered lines Coverage Common ancestor commit (627d421) 119294 86800 72.76% Head commit (b0dad27) 150544 (+31250) 98270 (+11470) 65.28% (-7.48%) Coverage variation is the difference between the coverage for the head and common ancestor commits of the pull request branch:
<coverage of head commit> - <coverage of common ancestor commit>Diff coverage details
Coverable lines Covered lines Diff coverage Pull request (#3962) 11 10 90.91% Diff coverage is the percentage of lines that are covered by tests out of the coverable lines that the pull request added or modified:
<covered lines added or modified>/<coverable lines added or modified> * 100%
NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes. Give us feedback
There was a problem hiding this comment.
Code Review
This pull request addresses issue #3952, where EXISTS { MATCH } subqueries incorrectly returned false for relationship types containing Cypher keyword fragments (e.g., WORKS_WITH). The fix introduces a helper method to include underscores in word boundary checks. Feedback suggests that the boundary logic should be further hardened to include characters like ':', '.', and '$' to prevent false matches in labels or properties. Additionally, it is recommended to apply the new identifier check to variable detection to ensure variables containing underscores are handled correctly.
| if (pos > 0 && isCypherIdentifierChar(upper.charAt(pos - 1))) | ||
| return false; | ||
| // Check word boundary after | ||
| final int end = pos + keyword.length(); | ||
| if (end < upper.length() && Character.isLetterOrDigit(upper.charAt(end))) | ||
| if (end < upper.length() && isCypherIdentifierChar(upper.charAt(end))) | ||
| return false; |
There was a problem hiding this comment.
The word boundary check for Cypher keywords is still incomplete. While adding the underscore fixes the reported issue for relationship types like WORKS_WITH, the logic will still incorrectly match keywords when they are part of a label (e.g., :WITH), a property access (e.g., .WITH), or a parameter (e.g., $WITH).
To be more robust, the boundary check should also consider characters like :, ., and $ as part of a token, which should prevent a keyword match.
if (pos > 0) {
final char c = upper.charAt(pos - 1);
if (isCypherIdentifierChar(c) || c == ':' || c == '.' || c == '$')
return false;
}
// Check word boundary after
final int end = pos + keyword.length();
if (end < upper.length()) {
final char c = upper.charAt(end);
if (isCypherIdentifierChar(c) || c == ':' || c == '.' || c == '$')
return false;
}There was a problem hiding this comment.
Fixed in commit b0dad27. Added ':' '.' '$' to the boundary checks in matchesKeywordAt alongside the existing isCypherIdentifierChar check, so [:WITH], n.with, and $with are no longer misidentified as standalone clause keywords.
Summary
Fixes #3952.
EXISTS { MATCH (p)-[:WORKS_WITH]->(:Person) }returnedfalsefor every row even when the pattern clearly matched, while the equivalentWHERE (p)-[:WORKS_WITH]->()predicate worked correctly.Root cause:
ExistsExpression.matchesKeywordAt()usedCharacter.isLetterOrDigit()for word-boundary detection. Because underscore_is not a letter or digit in Java,"WITH"inside"WORKS_WITH"was falsely recognised as the CypherWITHclause keyword. This causedinjectWhereConditions()to split the relationship type name mid-token, producing an invalid query such as:The resulting parse error was silently caught in
evaluate()and returnedfalse. Any relationship type whose name ends with a Cypher keyword after an underscore (_WITH,_WHERE,_RETURN,_ORDER,_SKIP,_LIMIT,_UNION) was affected.Fix: Replaced the
Character.isLetterOrDigit(c)boundary check inmatchesKeywordAt()with a newisCypherIdentifierChar(c)helper that also returnstruefor_, consistent with Cypher identifier rules.Changes
engine/src/main/java/com/arcadedb/query/opencypher/ast/ExistsExpression.java- fix inmatchesKeywordAt()+ newisCypherIdentifierChar()helperengine/src/test/java/com/arcadedb/query/opencypher/CypherExistsUnderscoreRelationshipTypeTest.java- regression test (3 cases:WORKS_WITH,KNOWS_WHERE, and controlKNOWS)Test plan
existsWithUnderscoreKeywordWithInRelationshipType- reproducesEXISTS { MATCH ... }subqueries may incorrectly returnfalsefor some relationship types even when the same pattern clearly matches. #3952 (WORKS_WITH embeds "WITH")existsWithUnderscoreKeywordWhereInRelationshipType- KNOWS_WHERE embeds "WHERE"existsWithSimpleRelationshipTypeStillWorks- control: KNOWS (no embedded keyword) still works🤖 Generated with Claude Code