From 33ee741422d4943776b59320fd3fa24096c7e0ce Mon Sep 17 00:00:00 2001 From: Tam Nguyen Duc <1218621+tamnd@users.noreply.github.com> Date: Sun, 16 Aug 2026 08:09:27 +0700 Subject: [PATCH] Refuse the write and transaction statements by name The reserved keyword list exists so that a statement the v0 core does not parse is turned away by name, and its comment says as much, but the list was missing the one statement most likely to be typed. GQL spells element creation INSERT, CREATE is the Cypher spelling of something GQL does not have, and only CREATE was listed. So INSERT (x:Person {name: 'Zoe'}) came back as expected MATCH, OPTIONAL MATCH, CALL, UNWIND, WITH, or RETURN, which sends a reader looking for a typo in a statement they spelled correctly. Adds INSERT, REMOVE, START, COMMIT, ROLLBACK, SESSION, FINISH and FOR alongside the keywords already there. Nothing new parses and no case changes outcome; eleven conformance cases that were failing on a misleading message now fail on an accurate one, which is where G3 and G8 will pick them up. Two tests. One walks every reserved statement and checks the refusal names the keyword. The other guards the risk the change carries: ORDER, SKIP and LIMIT belong to the projection in front of them, so reserving keywords at the head of a clause must not reach inside a RETURN that is parsing normally. --- crates/zu-query/src/parser.rs | 51 ++++++++++++++++++++++++++++++++++- 1 file changed, 50 insertions(+), 1 deletion(-) diff --git a/crates/zu-query/src/parser.rs b/crates/zu-query/src/parser.rs index 73199fcf..7c7bfe2b 100644 --- a/crates/zu-query/src/parser.rs +++ b/crates/zu-query/src/parser.rs @@ -65,8 +65,16 @@ fn endpoint(def: ElementTypeDef) -> Endpoint { /// Clause keywords the surface reserves but the v0 core does not parse /// yet; naming them beats "expected MATCH" when someone writes CREATE. +/// +/// The write and transaction statements are here rather than absent +/// because absence is the worse answer: INSERT is how GQL spells the +/// statement that adds an element, so a reader who writes one and is +/// told the parser expected MATCH has been sent looking for a typo +/// instead of a milestone. CREATE is in the list for the opposite +/// reason, being the Cypher spelling of a statement GQL does not have. const UNIMPLEMENTED: &[&str] = &[ - "CREATE", "SET", "DELETE", "DETACH", "MERGE", "FILTER", "LET", "NEXT", + "CREATE", "INSERT", "SET", "REMOVE", "DELETE", "DETACH", "MERGE", "FILTER", "LET", "NEXT", + "START", "COMMIT", "ROLLBACK", "SESSION", "FINISH", "FOR", ]; /// Parses one zuQL query. @@ -2212,6 +2220,47 @@ mod tests { assert!(parse_err("CREATE (n) RETURN n").contains("CREATE is not implemented yet")); } + /// A statement GQL defines and the v0 core does not parse should be + /// turned away by name. Being told the parser expected MATCH sends a + /// reader looking for a typo in a statement they spelled correctly, + /// which is the wrong place to look and the wrong thing to fix. + #[test] + fn a_statement_we_do_not_parse_yet_is_refused_by_name() { + for (source, kw) in [ + ("INSERT (x:Person {name: 'Zoe'})", "INSERT"), + ("MATCH (p) SET p.age = 37 RETURN p", "SET"), + ("MATCH (p) REMOVE p.age RETURN p", "REMOVE"), + ("MATCH (p) DELETE p", "DELETE"), + ("MATCH (p) DETACH DELETE p", "DETACH"), + ("START TRANSACTION READ WRITE", "START"), + ("COMMIT", "COMMIT"), + ("ROLLBACK", "ROLLBACK"), + ("SESSION SET VALUE $x = 1", "SESSION"), + ("MATCH (p) FINISH", "FINISH"), + ("FOR x IN [1, 2] RETURN x", "FOR"), + ] { + let err = parse_err(source); + assert!( + err.contains(&format!("{kw} is not implemented yet")), + "{source:?} was refused with {err:?}, which does not name {kw}" + ); + } + } + + /// ORDER BY, SKIP and LIMIT belong to the projection that precedes + /// them, so reserving statement keywords at the head of a clause + /// must not reach inside a RETURN that is parsing normally. + #[test] + fn reserving_statement_keywords_leaves_the_projection_alone() { + for source in [ + "MATCH (n) RETURN n ORDER BY n.age", + "MATCH (n) RETURN n ORDER BY n.age DESC LIMIT 3", + "MATCH (n) WITH n ORDER BY n.age RETURN n", + ] { + parsed(source); + } + } + /// The label expression on the first node of the first pattern. fn label_of(source: &str) -> LabelExpr { let Clause::Match { patterns, .. } = &parsed(source).clauses[0] else {