feat(parser): write clauses + full wire-up — CREATE, MERGE, SET, REMOVE, DELETE, CALL#658
Conversation
…VE, DELETE, CALL (#653) - `parse_query` dispatches all 13 clause types (was 7) - CREATE: pattern list - MERGE: single pattern + optional ON CREATE/ON MATCH SET - SET: property assignment, full replace, property merge, label addition - REMOVE: property removal, label removal - DELETE / DETACH DELETE: comma-separated expression list - CALL: subquery form `CALL { ... }` and named procedure `CALL f(args)` with optional YIELD - 13 tests for write clauses; `all_clause_types_dispatch_without_panic` confirms all 13 dispatch arms reachable Closes #653 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
Caution Review failedThe pull request is closed. ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (1)
WalkthroughParser adds complete write-clause support to ChangesWrite-clause parsing
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related issues
Possibly related PRs
Suggested labels
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #658 +/- ##
=======================================
Coverage 84.09% 84.09%
=======================================
Files 49 49
Lines 12921 12921
Branches 3628 3628
=======================================
Hits 10866 10866
Misses 1270 1270
Partials 785 785
Flags with carried forward coverage won't be shown. Click here to find out more.
Continue to review full report in Codecov by Sentry.
🚀 New features to boost your workflow:
|
- CALL subquery: error on unterminated brace block (depth != 0 after loop) - MERGE: accumulate ON CREATE/MATCH SET items with extend instead of overwriting - SET/REMOVE: capture exact VarRef span from token triple, not start+1 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (3)
crates/gf-cypher/src/parser/clauses.rs (3)
1048-1056: ⚡ Quick winTest doesn't verify subquery content is preserved.
The test confirms
procedure.is_empty()andyield_items.len() == 1, but doesn't verify that the inner queryMATCH (n) RETURN nwas actually parsed. Once the subquery parsing issue is fixed, add assertions onc.subqueryto ensure the inner query's clauses are present.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@crates/gf-cypher/src/parser/clauses.rs` around lines 1048 - 1056, The test call_subquery currently only checks procedure.is_empty() and yield_items length; update it to assert that the subquery was parsed by inspecting c.subquery (the inner query AST) — e.g., ensure c.subquery is Some(_) and that its clauses contain the MATCH and RETURN clause nodes (check the number of clauses and/or inspect the first clause is a MATCH and the last is a RETURN) so the inner query "MATCH (n) RETURN n" is preserved in the AstClause::Call(c) structure.
249-268: 💤 Low valueDuplicate
ON CREATE SET/ON MATCH SETsilently overwrites.The loop allows multiple
ON CREATE SETorON MATCH SETclauses but replaces the previous items instead of extending them or erroring. If a query containsMERGE (n) ON CREATE SET n.x = 1 ON CREATE SET n.y = 2, only the second action's items are retained.Consider either extending the vectors or detecting and rejecting duplicates:
Option: Error on duplicate
loop { if ts.at(&Tok::On) { ts.advance(); // consume ON match ts.peek() { Some(Tok::Create) => { + if !on_create.is_empty() { + return Err(ts.err("duplicate ON CREATE SET")); + } ts.advance(); // consume CREATE ts.eat(&Tok::Set)?; on_create = parse_set_items(ts)?; } Some(Tok::Match) => { + if !on_match.is_empty() { + return Err(ts.err("duplicate ON MATCH SET")); + } ts.advance(); // consume MATCH ts.eat(&Tok::Set)?; on_match = parse_set_items(ts)?; }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@crates/gf-cypher/src/parser/clauses.rs` around lines 249 - 268, The loop handling ON clauses currently overwrites previous ON CREATE SET and ON MATCH SET data (variables on_create and on_match) because each parse_set_items(ts) replaces the vector; update the handling in the loop in parse of clauses.rs so duplicates are either rejected or merged: either (a) check if on_create/on_match is already Some and return an error via ts.err("duplicate ON CREATE/MATCH SET") when ts.at(&Tok::On) -> Tok::Create/Tok::Match is seen a second time, or (b) when on_create/on_match is Some, append/extend the existing Vec with the result of parse_set_items(ts) instead of replacing it (use Vec::extend). Locate the logic around the loop that inspects ts.at(&Tok::On), ts.advance(), match on Tok::Create/Match, and variables on_create/on_match to implement the chosen fix.
318-329: 💤 Low valueVarRef span is hardcoded to 1 character.
Span::new(start, start + 1)assumes the variable name is exactly one character. For multi-character names likeperson.name = 'x', the VarRef span will be incorrect. This affects error reporting and source mapping but not parsing correctness.Consider capturing the position before and after
eat_identto derive the accurate span, or haveeat_identreturn(String, Span).Also applies to: 398-408
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@crates/gf-cypher/src/parser/clauses.rs` around lines 318 - 329, The VarRef span is being hardcoded with Span::new(start, start + 1) inside the SetItem::Property construction which assumes single-character variable names; update the code to compute the correct span by capturing the identifier bounds (either have eat_ident return (String, Span) or capture position before/after eat_ident) and use that span when constructing VarRef (and similarly fix the other occurrence around lines 398-408); look for PropertyAccess/Expr::Var/VarRef usages and replace the hardcoded Span::new(...) with the precise span returned or computed from eat_ident.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@crates/gf-cypher/src/parser/clauses.rs`:
- Around line 454-483: The CALL { ... } branch currently discards the inner
query by simply consuming tokens and returning an empty CallClause; update
parsing to capture and parse the subquery into the CallClause (add a subquery:
Option<AstQuery> field in the AST) instead of leaving procedure/args empty: when
ts.at(&Tok::LBrace) advance past `{`, collect or slice the token range for the
brace-balanced body and invoke the existing query parser (or appropriate
parse_query function) to produce an AstQuery and store it in
CallClause.subquery; if the closing `}` is never found, return a parse error
instead of silently exiting; preserve existing parse_opt_yield(ts)? handling
after parsing the subquery and set span via ts.span_from(start).
---
Nitpick comments:
In `@crates/gf-cypher/src/parser/clauses.rs`:
- Around line 1048-1056: The test call_subquery currently only checks
procedure.is_empty() and yield_items length; update it to assert that the
subquery was parsed by inspecting c.subquery (the inner query AST) — e.g.,
ensure c.subquery is Some(_) and that its clauses contain the MATCH and RETURN
clause nodes (check the number of clauses and/or inspect the first clause is a
MATCH and the last is a RETURN) so the inner query "MATCH (n) RETURN n" is
preserved in the AstClause::Call(c) structure.
- Around line 249-268: The loop handling ON clauses currently overwrites
previous ON CREATE SET and ON MATCH SET data (variables on_create and on_match)
because each parse_set_items(ts) replaces the vector; update the handling in the
loop in parse of clauses.rs so duplicates are either rejected or merged: either
(a) check if on_create/on_match is already Some and return an error via
ts.err("duplicate ON CREATE/MATCH SET") when ts.at(&Tok::On) ->
Tok::Create/Tok::Match is seen a second time, or (b) when on_create/on_match is
Some, append/extend the existing Vec with the result of parse_set_items(ts)
instead of replacing it (use Vec::extend). Locate the logic around the loop that
inspects ts.at(&Tok::On), ts.advance(), match on Tok::Create/Match, and
variables on_create/on_match to implement the chosen fix.
- Around line 318-329: The VarRef span is being hardcoded with Span::new(start,
start + 1) inside the SetItem::Property construction which assumes
single-character variable names; update the code to compute the correct span by
capturing the identifier bounds (either have eat_ident return (String, Span) or
capture position before/after eat_ident) and use that span when constructing
VarRef (and similarly fix the other occurrence around lines 398-408); look for
PropertyAccess/Expr::Var/VarRef usages and replace the hardcoded Span::new(...)
with the precise span returned or computed from eat_ident.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 69e1d900-6fb7-49ac-a39c-48be6acac66f
📒 Files selected for processing (1)
crates/gf-cypher/src/parser/clauses.rs
| if ts.at(&Tok::LBrace) { | ||
| // CALL { ... } subquery form | ||
| ts.advance(); // consume { | ||
| // Consume tokens until matching } (depth-aware) | ||
| let mut depth = 1usize; | ||
| while !ts.is_empty() { | ||
| match ts.peek() { | ||
| Some(Tok::LBrace) => { | ||
| depth += 1; | ||
| ts.advance(); | ||
| } | ||
| Some(Tok::RBrace) => { | ||
| depth -= 1; | ||
| ts.advance(); | ||
| if depth == 0 { | ||
| break; | ||
| } | ||
| } | ||
| _ => { | ||
| ts.advance(); | ||
| } | ||
| } | ||
| } | ||
| let yield_items = parse_opt_yield(ts)?; | ||
| Ok(CallClause { | ||
| procedure: vec![], | ||
| args: vec![], | ||
| yield_items, | ||
| span: ts.span_from(start), | ||
| }) |
There was a problem hiding this comment.
CALL subquery body is discarded — the inner query is never parsed or stored.
The subquery form consumes all tokens between { and } using depth tracking, but the tokens are simply advanced past without being parsed. The resulting CallClause has no representation of the subquery content (procedure: vec![], args: vec![]).
For CALL { MATCH (n) RETURN n } YIELD n, the inner MATCH (n) RETURN n is lost entirely. This makes the AST incomplete and downstream processing (IR, execution) impossible.
Additionally, if the closing } is never found, the loop exits silently when ts.is_empty() without producing an error, leading to confusing downstream failures.
Suggested approach
if ts.at(&Tok::LBrace) {
// CALL { ... } subquery form
ts.advance(); // consume {
- // Consume tokens until matching } (depth-aware)
- let mut depth = 1usize;
- while !ts.is_empty() {
- match ts.peek() {
- Some(Tok::LBrace) => {
- depth += 1;
- ts.advance();
- }
- Some(Tok::RBrace) => {
- depth -= 1;
- ts.advance();
- if depth == 0 {
- break;
- }
- }
- _ => {
- ts.advance();
- }
- }
- }
+ // Recursively parse the inner query
+ let subquery = parse_query(ts)?;
+ if !ts.eat_if(&Tok::RBrace) {
+ return Err(ts.err("expected `}` to close CALL subquery"));
+ }
let yield_items = parse_opt_yield(ts)?;
Ok(CallClause {
procedure: vec![],
args: vec![],
+ subquery: Some(subquery),
yield_items,
span: ts.span_from(start),
})This requires CallClause in gf-ast to have a subquery: Option<AstQuery> field.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@crates/gf-cypher/src/parser/clauses.rs` around lines 454 - 483, The CALL {
... } branch currently discards the inner query by simply consuming tokens and
returning an empty CallClause; update parsing to capture and parse the subquery
into the CallClause (add a subquery: Option<AstQuery> field in the AST) instead
of leaving procedure/args empty: when ts.at(&Tok::LBrace) advance past `{`,
collect or slice the token range for the brace-balanced body and invoke the
existing query parser (or appropriate parse_query function) to produce an
AstQuery and store it in CallClause.subquery; if the closing `}` is never found,
return a parse error instead of silently exiting; preserve existing
parse_opt_yield(ts)? handling after parsing the subquery and set span via
ts.span_from(start).
Closes #653
Part of #554 (chunk D of 4, final parser chunk).
Summary
parse_querynow dispatches all 13 clause types (7 read-only from parser: read-only clauses — MATCH, OPTIONAL MATCH, WHERE, WITH, RETURN, UNWIND, UNION (src/parser/clauses.rs) #652 + 6 write clauses here)ON CREATE SET/ON MATCH SETn.x = v), full replace (n = {}), property merge (n += {}), label addition (n:Label)n.x), label removal (n:Label)CALL { ... } [YIELD ...]and named procedureCALL f.name(args) [YIELD ...]grammar.lalrpopandbuild.rswere already gone (cleaned up in earlier PRs)Test plan
CREATE (n:Person {name: 'Alice'})parses correctlyCREATE (a)-[:KNOWS]->(b)— relationship creationMERGE (n:Person {email: $e}) ON CREATE SET n.created = 1 ON MATCH SET n.seen = 2SET n.age = 30, n.name = 'Bob'— multiple set itemsSET n = {age: 30}— full replacementSET n:Employee— label additionREMOVE n.age, n:TempLabel— mixed removeDELETE n, rDETACH DELETE nCALL { MATCH (n) RETURN n } YIELD n— subquery formCALL db.labels() YIELD label— procedure formparse_querywithout paniccargo test -p gf-cypher— 102 tests passcargo test --workspace— full workspace green🤖 Generated with Claude Code
Note
Add write clause parsing (CREATE, MERGE, SET, REMOVE, DELETE, CALL) to Cypher parser
parse_queryin clauses.rs to dispatch all major write clauses instead of returning errors for them.CREATE(pattern list),MERGE(single pattern with optionalON CREATE SET/ON MATCH SETblocks),SET(all item forms: property assign, full replace, merge, label add),REMOVE(property and label forms),DELETE/DETACH DELETE(expression list), andCALL(subquery block or named procedure with optionalYIELD).parse_set_items,parse_remove_items,parse_opt_yield,parse_procedure_name,eat_label_names, andeat_ident_with_span.Macroscope summarized 85e01be.
Summary by CodeRabbit