Asking in Cypher
Compiling Cypher into a jaala program over a host's graph view, or one a node, edge and attr store writes itself: what it compiles, where its answers differ from a Cypher engine's, and what it refuses with the issue that would let it.
Someone who knows a graph query language, or a model that has read a lot of it, can ask in Cypher, and github.com/panyam/jaala/frontends/cypher compiles the query into a jaala program the engine answers like any other (#352, mission #356). It’s a module of its own, with no dependency beyond jaala. The program it writes is ordinary jaala text, so Validate, Eval, Explain and Locate read it as they read one written by hand.
package guide_test
import (
"context"
"errors"
"fmt"
"strings"
"github.com/panyam/jaala/datalog"
"github.com/panyam/jaala/frontends/cypher"
"github.com/panyam/jaala/frontends/nl"
"github.com/panyam/jaala/ns"
)
// depsGraph reads the guide's relations as a property graph: a Package is a
// pkg fact keyed by its name, with its license as a property, and IMPORTS is
// an imports fact from its first argument to its second.
var depsGraph = cypher.Graph{
Nodes: []cypher.Node{
{Label: "Package", Relation: "pkg", Key: 0, Props: map[string]int{"name": 0, "license": 1}},
},
Edges: []cypher.Edge{
{Type: "IMPORTS", Relation: "imports", From: 0, To: 1},
},
}
// runCypher compiles a query, prints the program it compiled to, and runs
// that program, binding the query's $parameters.
func runCypher(text string, params map[string]ns.Value) {
v, err := vocabulary(source())
if err != nil {
panic(err)
}
p, err := cypher.Compile(v, depsGraph, text)
if err != nil {
fmt.Println(err)
return
}
fmt.Println(p.Text)
var opts []datalog.Option
if params != nil {
bind, err := p.Bind(params)
if err != nil {
panic(err)
}
opts = append(opts, bind)
}
t, err := datalog.SemiNaive{}.EvalTable(context.Background(), p.Query, datalog.MustBase(v, source()), opts...)
if err != nil {
fmt.Println(err)
return
}
fmt.Println(strings.Join(p.Columns, " | "))
for i := range t.Len() {
r := t.Row(i)
var vals []string
for _, c := range t.Columns {
vals = append(vals, show(r.Bind[c]))
}
fmt.Println(strings.Join(vals, " | "))
}
}
// Compile turns Cypher into a jaala program over the graph view; the program
// is ordinary jaala, run like any other.
func Example_cypher() {
runCypher(`MATCH (a:Package {name: 'app'})-[:IMPORTS*]->(b:Package)
RETURN b.name AS dependency, b.license AS license ORDER BY dependency`, nil)
// Output:
// pkg("app", _), pkg(?b, ?b_license), graph.closure("imports", "app", ?b, 1, 0, _) => ?b, ?b_license order by ?b
// dependency | license
// "api" | absent
// "log" | ""
// "util" | "Apache-2.0"
}
// A $parameter is a goal variable, bound with Program.Bind.
func Example_cypherParameter() {
runCypher(`MATCH (p:Package)-[:IMPORTS]->(:Package {name: $target}) RETURN p.name`,
map[string]ns.Value{"target": ns.S("log")})
// Output:
// pkg(?p, _), pkg(?target, _), imports(?p, ?target) => ?p
// p.name
// "api"
}
// A WITH closes a stage, a rule whose head is what it hands on, so its
// aggregate can be filtered as Cypher's HAVING. A parameter such a rule reads
// is out of Bind's reach, so cypher.Params writes its value in.
func Example_cypherWith() {
runCypher(`MATCH (a:Package)-[:IMPORTS*]->(d:Package)
WITH a, count(DISTINCT d) AS n WHERE n > 1
RETURN a.name AS name, n ORDER BY n DESC`, nil)
v, err := vocabulary(source())
if err != nil {
panic(err)
}
q := `MATCH (:Package {name: $pkg})-[:IMPORTS*]->(d) WITH d RETURN d.name`
_, err = cypher.Compile(v, depsGraph, q)
fmt.Println(err)
p, err := cypher.Compile(v, depsGraph, q, cypher.Params(map[string]ns.Value{"pkg": ns.S("api")}))
if err != nil {
panic(err)
}
fmt.Println(p.Text)
// Output:
// cypher_with1(?a, count(distinct ?d)) :- pkg(?a, _), pkg(?d, _), graph.closure("imports", ?a, ?d, 1, 0, _);
// cypher_with1(?a, ?n), ?n > 1 => ?a, ?n order by ?n desc
// name | n
// "app" | "3"
// "api" | "2"
// cypher: $pkg is read inside a WITH stage, a UNION part, an OR, a pattern test, a variable-length relationship or an OPTIONAL MATCH, which datalog.Bind can't reach: compile with cypher.Params
// cypher_with1(?d) :- pkg("api", _), graph.closure("imports", "api", ?d, 1, 0, _);
// cypher_with1(?d) => ?d
}
// COUNT { } counts a pattern's matches from each row's nodes, a node with none
// counting 0, and UNION joins parts returning the same columns.
func Example_cypherCountAndUnion() {
runCypher(`MATCH (p:Package) RETURN p.name AS name, COUNT { (p)<-[:IMPORTS]-() } AS importers ORDER BY name`, nil)
runCypher(`MATCH (p:Package {license: 'MIT'}) RETURN p.name AS name
UNION MATCH (p:Package) WHERE p.license IS NULL RETURN p.name AS name`, nil)
// Output:
// pkg(?p, _), ?_count = count(?_n) : { imports(?_n, ?p) } => ?p, ?_count order by ?p
// name | importers
// "api" | "1"
// "app" | "0"
// "log" | "1"
// "util" | "1"
// cypher_union1(?p) :- pkg(?p, "MIT");
// cypher_union1(?p) :- pkg(?p, ?p_license), absent(?p_license);
// cypher_union1(?name) => ?name
// name
// "api"
// "app"
}
// OPTIONAL MATCH keeps a row whose optional part doesn't match, with null
// (jaala's absent) in its place, and count over an optional node counts only
// its matches, as Cypher's skips a null.
func Example_cypherOptional() {
runCypher(`MATCH (p:Package) OPTIONAL MATCH (p)-[:IMPORTS]->(d) RETURN p.name AS name, d.name AS imports`, nil)
runCypher(`MATCH (p:Package) OPTIONAL MATCH (p)<-[:IMPORTS]-(i) RETURN p.name AS name, count(i) AS importers`, nil)
// Output:
// pkg(?p, _), optional { imports(?p, ?d) } => ?p, ?d
// name | imports
// "api" | "log"
// "app" | "api"
// "log" | "util"
// "util" | absent
// pkg(?p, _), optional { imports(?i, ?p), ?_seen = 1 * 1 } => ?p, sum(?_seen)
// name | importers
// "api" | "1"
// "app" | "0"
// "log" | "1"
// "util" | "1"
}
// Cypher a program can't express yet is refused with the issue that would
// let it, so a model or a caller knows to rewrite rather than retry.
func Example_cypherGap() {
v, err := vocabulary(source())
if err != nil {
panic(err)
}
q := `MATCH (p:Package) SET p.license = 'MIT' RETURN p`
p, err := cypher.Compile(v, depsGraph, q)
var ce *cypher.Error
if errors.As(err, &ce) {
fmt.Printf("gap #%d\n", ce.Gap)
}
fmt.Print(cypher.Explain(q, p, err))
// Output:
// gap #287
// cypher: not supported yet: writing to the graph (jaala#287): SET
// line 1, column 19:
// MATCH (p:Package) SET p.license = 'MIT' RETURN p
// ^
// Write the query without it.
}
// A store keeping every node in node(id, kind), every relationship in
// edge(from, kind, to) and properties in attr(id, key, value) needs no graph
// written by hand: cypher.Store reads its kinds and writes one.
func Example_cypherStore() {
src := ns.NewMemSource().
Declare("node", "id", "kind").
Declare("edge", "from", "kind", "to").
Declare("attr", "id", "key", "value")
add := func(rel string, vals ...string) {
var t ns.Tuple
for _, v := range vals {
t.Vals = append(t.Vals, ns.S(v))
}
src.Add(rel, t)
}
add("node", "f:main", "func")
add("node", "f:parse", "func")
add("node", "t:main", "test")
add("edge", "f:main", "calls", "f:parse")
add("edge", "t:main", "covers", "f:main")
add("attr", "f:parse", "doc", "reads the input")
v := ns.MustVocabulary(src)
b := datalog.MustBase(v, src)
store := cypher.Store{Node: "node", Edge: "edge", Attr: "attr"}
nodeKinds, edgeKinds, err := store.Kinds(context.Background(), b)
if err != nil {
panic(err)
}
g := store.Graph(nodeKinds, edgeKinds)
var names []string
for _, n := range g.Nodes {
names = append(names, n.Label)
}
for _, e := range g.Edges {
names = append(names, e.Type)
}
fmt.Println(strings.Join(names, " "))
p, err := cypher.Compile(v, g, `MATCH (t:Test)-[:COVERS]->(:Func)-[:CALLS]->(f:Func) RETURN t, f, f.doc`)
if err != nil {
panic(err)
}
rows, err := datalog.SemiNaive{}.Eval(context.Background(), p.Query, b)
if err != nil {
panic(err)
}
for _, r := range rows {
var vals []string
for _, c := range p.Query.Columns() {
vals = append(vals, show(r.Bind[c]))
}
fmt.Println(strings.Join(vals, " | "))
}
// Output:
// Func Test CALLS COVERS
// "t:main" | "f:parse" | "reads the input"
}
// A property the host computes rather than stores, here a node's short name
// from its id, is read through a relation of its own: Derived names it, and
// each read is an atom over it, so the predicate answers by name or by node.
func Example_cypherDerived() {
src := ns.NewMemSource().
Declare("node", "id", "kind").
Declare("edge", "from", "kind", "to")
src.Add("node", ns.Tuple{Vals: []ns.Value{ns.S("f:main"), ns.S("func")}})
src.Add("node", ns.Tuple{Vals: []ns.Value{ns.S("f:parse"), ns.S("func")}})
src.Add("edge", ns.Tuple{Vals: []ns.Value{ns.S("f:main"), ns.S("calls"), ns.S("f:parse")}})
v := ns.MustVocabulary(src)
short := func(id string) string { return id[strings.Index(id, ":")+1:] }
err := v.AddPredicate("name.of", ns.Builtin{
Arity: 2,
Modes: [][]bool{{true, false}, {false, true}},
Gen: func(_ context.Context, _ ns.Source, args []ns.Arg, emit func([]ns.Value, []string) error) error {
if args[0].Bound {
return emit([]ns.Value{args[0].Value, ns.S(short(args[0].Value.S))}, nil)
}
// By name: a real host asks its name index.
for _, id := range []string{"f:main", "f:parse"} {
if short(id) == args[1].Value.S {
if err := emit([]ns.Value{ns.S(id), args[1].Value}, nil); err != nil {
return err
}
}
}
return nil
},
})
if err != nil {
panic(err)
}
b := datalog.MustBase(v, src)
store := cypher.Store{Node: "node", Edge: "edge",
Derived: map[string]cypher.PropSource{"name": {Relation: "name.of", Node: 0, Value: 1}}}
g := store.Graph([]string{"func"}, []string{"calls"})
p, err := cypher.Compile(v, g, `MATCH (f:Func {name: 'parse'})<-[:CALLS]-(g) RETURN g, g.name`)
if err != nil {
panic(err)
}
fmt.Println(p.Text)
rows, err := datalog.SemiNaive{}.Eval(context.Background(), p.Query, b)
if err != nil {
panic(err)
}
for _, r := range rows {
var vals []string
for _, c := range p.Query.Columns() {
vals = append(vals, show(r.Bind[c]))
}
fmt.Println(strings.Join(vals, " | "))
}
// Output:
// node(?f, "func"), edge(?g, "calls", ?f), name.of(?f, "parse"), name.of(?g, ?g_name) => ?g, ?g_name
// "f:main" | "main"
}
// With the Cypher front end, Translate asks a model for Cypher: the prompt
// is the graph and Cypher's instructions, each draft is compiled before it
// is checked, and a refusal goes back in Cypher's terms.
func Example_cypherTranslate() {
v, err := vocabulary(source())
if err != nil {
panic(err)
}
m := &scriptedModel{drafts: []string{
`MATCH (:Pakage {name: 'app'})-[:IMPORTS*]->(d) RETURN d.name`,
`MATCH (:Package {name: 'app'})-[:IMPORTS*]->(d) RETURN d.name`,
}}
fe := cypher.NewFrontEnd(depsGraph)
p, tr, err := nl.Translate(context.Background(), "what does app depend on?", v, nil, m,
nl.Options{Base: datalog.MustBase(v, source()), FrontEnd: fe})
if err != nil {
fmt.Println(err)
return
}
for _, t := range tr.Tries {
fmt.Printf("try in %s: %q\n", t.Language, t.Kind)
}
fmt.Println(p.Text)
fmt.Println("compiled to:", strings.Join(strings.Fields(p.Compiled), " "))
for _, r := range p.Rows {
fmt.Print(r.Bind["d"].S, " ")
}
fmt.Println()
// Output:
// told: cypher: no label Pakage in this graph, did you mean "Package"? (its labels: Package)
// line 1, column 7:
// MATCH (:Pakage {name: 'app'})-[:IMPORTS*]->(d) RETURN d.name
// ^
//
// Write the whole query again, fixed.
//
// try in cypher: "compile"
// try in cypher: ""
// MATCH (:Package {name: 'app'})-[:IMPORTS*]->(d) RETURN d.name
// compiled to: pkg("app", _), graph.closure("imports", "app", ?d, 1, 0, _) => ?d
// api log util
}
The graph view ¶
A relation’s schema doesn’t say which of its arguments is an endpoint, so the host writes a cypher.Graph saying how its relations read as nodes, relationships and properties:
- A
Nodeis the relation holding one label’s nodes, the argument holding a node’s key, and its properties by argument. A node is its key value, so(p:Package)bindspto a package’s name, andRETURN panswers it. A property at the key reads the key itself, so{name: 'app'}is the nodeapp. - An
Edgeis the relation holding one relationship type, readFromone argumentToanother, with its own properties. Fixedpins arguments to constants, for a store that keeps every label in one relation and tells them apart by a value (node(id, "Function")), andPropsnames a key-value relation,attr(node, key, value), for a property noNodelists.Derivednames, per property, a relation or predicate holding it for any node,name.of(node, value), for one the host computes rather than stores, such as a function’s short name worked out from its id (#378). Each read is an atom over it,{name: 'Foo'}asname.of(?f, "Foo")andRETURN f.nameasname.of(?f, ?v), so the predicate’s modes say which the host answers. A label’s own column wins over it, and it wins overProps.
Graph.Validate(v) checks each relation is a member of the vocabulary at an arity holding every argument named, and Compile checks it first.
A node, edge and attr store ¶
Declaire and Agni keep their graphs the same way, every node in node(id, kind), every relationship in edge(from, kind, to), and properties in attr(id, key, value), and writing a Node and an Edge for each kind by hand gets old quickly. cypher.Store names the three relations and Store.Graph(nodeKinds, edgeKinds) writes the graph: a label per node kind, keyed by id with the kind fixed, a type per edge kind from its first argument to its third, and attr as the property relation (#366), with Store.Derived as the graph’s Derived. Store.Kinds(ctx, base) reads the kinds from the data, for a host whose kinds are its data, as the last example above does. It reads both relations whole, so a host asks once per data version.
A kind becomes a label the way labels are usually written, func as Func and http_route as HttpRoute (cypher.DefaultLabel), and a type upper case with underscores, dispatches_to as DISPATCHES_TO (cypher.DefaultType). A host with other conventions sets Store.Label or Store.Type. Two kinds that come out as one label make a graph Validate refuses, since a label would mean two things.
The graph is a description of the vocabulary rather than anything Cypher’s, so it may move into jaala’s core when a second reader arrives, such as a GQL front end or drawing a dataset as a graph.
What it compiles ¶
cypher.Compile(v, graph, text) returns a cypher.Program: the program’s Text, its parsed Query, the answer’s Columns as Cypher names them, and the goal variable each $parameter is, which Program.Bind turns into a datalog.Bind option. Helper relations it writes start with cypher_: the recursive cypher_reach1 the first example’s variable-length step became, a cypher_with rule per WITH stage, a cypher_match rule per alternative of an OR, a cypher_exists rule for a pattern or EXISTS { } in WHERE, and a cypher_union relation for a UNION.
A Bind reaches only the goal, and a rule’s variables are its own, so a $parameter a rule reads (one inside a WITH stage, a UNION part, an OR, a pattern test, a variable-length step or an OPTIONAL MATCH) is refused unless the host compiles with cypher.Params(values), which writes each value in as a constant, as the WITH example’s second program shows. That’s one compile per set of values, which suits a translator compiling each question once.
It compiles the part of Cypher a program expresses today, which is mostly the read-only core: MATCH over node and relationship patterns (labels, inline properties, either direction, several types, variable length, *1..6 or a quantifier such as ->{1,6} or ->+, as graph.closure, and shortestPath or a path selector such as SHORTEST 1, which keep the match’s nodes), a path variable over one relationship, p = (a)-[:IMPORTS*]->(b), as graph.paths (each trail as a value, or with shortestPath one shortest path per pair, read by length(p), nodes(p), relationships(p) and x IN nodes(p)), OPTIONAL MATCH after a MATCH, as an optional { } holding its nodes, relationships and WHERE, WHERE (comparisons, AND, OR, XOR, NOT, IS NULL on a property or a variable, STARTS WITH, ENDS WITH, CONTAINS, =~, IN over a written list, a label test, a pattern, EXISTS { } and COUNT { }, the last a body aggregate so a node with no match counts 0), lists as values (collect builds one, a WITH carries it, and size, head, last, l[i], x IN l and UNWIND l AS x read it through the list.* functions, with list literals of constants and IN $param, #283), relationship variables ([r:CALLS] binds the edge’s fact, as edge(?f, "CALLS", ?g) as ?r, so RETURN r answers its id, type(r) its type, startNode(r) and endNode(r) its ends, and r.prop a column of its type or the property relation keyed by its id; a type the vocabulary derives has no facts, so its id and properties are refused, #441; a WITH carries one with its type and ends, a subquery binds its own or matches the MATCH’s again, and p = (a)-[r:T]->(b) binds r beside the path, #445), and RETURN with aliases, DISTINCT, count, min, max, sum, collect and constant columns (RETURN f.name, 'yes' AS found, #454), then ORDER BY, SKIP and LIMIT. A test returned as a value, RETURN f.name IN [...] AS hit, is refused with the rewrite: jaala has no booleans, so the test goes in WHERE and keeps the rows it holds for.
Past one MATCH and RETURN, WITH closes a stage with its aggregates and its WHERE (Cypher’s way of filtering on a count), and a node it hands on keeps its labels, so a property read after it still finds its column. UNION and UNION ALL join parts that return the same columns. CALL proc(args) YIELD name, ... reads one of the vocabulary’s predicates or relations, each YIELD landing on the argument its label names, so CALL str.split(p.license, '-') YIELD index, part is str.split. And toLower, toUpper, replace, split(s, sep)[i] and + between text compile to the standard library’s str.*; + joins when a side is a string, a string function or a property the schema types as text, and adds otherwise.
Where its answers differ ¶
We’d rather say where a program’s answer can’t be Cypher’s than have the compiler guess, so here they are:
- The rows are distinct, as a program’s answer is:
RETURNwithoutDISTINCTanswers each row once, andcount(x)counts distinct bindings of the match, not paths. Two relationships between one pair of nodes are two bindings only when a relationship variable names them, which binds each edge’s fact (#441), and only over a type the host stores as facts. - A property read through the graph’s
Propsrelation, or aDerivedone, is a match: a node without it has no row, where Cypher would answer null, unless anOPTIONAL MATCHdeclared the node, whose propertiesRETURNreads are read optionally (#400). - Null is jaala’s
absent, and the compiler writes Cypher’s rules for it into the program. A comparison or a text test over a value that may be absent (a label’s column, a node or value anOPTIONAL MATCHbound, a value aWITHcarried) gets anot absent(...)guard, so it filters its row as Cypher’s unknown does, andcount(b)over an optional node sums a marker its braces bind, so a row with nothing matched counts 0.count(p.x)over a column that may be absent still counts the node (#400). - A variable-length match without a path variable is reachability: it doesn’t keep Cypher’s rule that a path uses a relationship once, which can differ for a bounded range over a cycle. Bind the path,
p = (...), for trails (#280). A zero-length one from a node nothing else binds pairs only the nodes its relationship’s relation holds, where Cypher’s would pair every node with itself. - Within one
MATCH, Cypher uses a relationship once, where a program’s atoms may read one fact twice: a self-loop stands for both hops of(a)-->(b)-->(c), which Cypher doesn’t match. - A path is its nodes, so two relationships between the same two nodes make one path and
count(p)counts routes, unless the query readsrelationships(p), when each path names its edges’ facts and they make two. minandmaxreduce numbers, as jaala’s aggregates do, so over text they answer null where Cypher answers the least or greatest string.collectbuilds a list sorted in jaala’s answer order rather than in match order, so it doesn’t depend on the order rows arrive in, and lists compare and order by their text (#405).
These are checked against Neo4j itself (#413). stores/bolt’s TestCypherAgreesWithNeo4j writes seeded queries over seeded graphs built so none of the differences above can arise (no self-loops; one relationship at most from one node to another; every node holding every property), loads each graph into Neo4j through the Bolt store, and runs the same text through the compiler and natively. Any answer that differs is either a difference to list here or a bug, and the test keeps the open ones in a list with the issue that fixes each, so it still fails on a new one.
What it refuses ¶
The rest it refuses with a *cypher.Error carrying the Pos (line, column and byte offset) in the Cypher text, and, for Cypher a program can’t express yet, the Gap: the jaala issue that would let it, with a Capability in words a model can act on, as the last example shows. Those are a relationship variable on a variable-length relationship, a path variable over more than one relationship, ALL SHORTEST or SHORTEST k paths as values, shortestPath written as a value rather than bound to a path variable, and quantified path patterns (#398), an OPTIONAL MATCH before any MATCH, with an OR in its WHERE, adding a label to a node an earlier match bound, or reading a relationship variable it binds, which may be null (#400), maps, comprehensions and lists of more than constants (#405), functions beyond the aggregates above (#284), writes (#287), USE and LOAD CSV (#289), CALL { } subqueries, an ORDER BY, SKIP or LIMIT inside a WITH or a UNION part, a label expression with |, ! or %, and a query with no MATCH before its RETURN or no RETURN at all (#364), since a rule holds a relation, with no order or limit to keep, and a WHERE that expands to more than 16 alternatives, such as a chain of XORs (#388). A Gap of 0 is a mistake in the text or against the graph, such as a label it doesn’t have, or what isn’t a query (SHOW, CREATE INDEX, EXPLAIN).
The parser reads all of Neo4j 5’s query syntax (#382), comprehensions, CASE, quantified paths, IS :: and the writing clauses included, so valid Cypher is never refused as a typo. It reports the first refusal only once the whole text has parsed: a mistake anywhere in the query is reported before a gap, so a model fixing the gap doesn’t meet the typo on its next try. A WHERE inside a node or relationship of a MATCH ((p:Package WHERE p.license = 'MIT')), IS Label, A&B labels, hex and octal integers, OFFSET, RETURN *, x and a CYPHER or PROFILE prefix compile.
A model writing Cypher ¶
cypher.NewFrontEnd(graph, examples...) is the front end nl.Translate takes in Options.FrontEnd, so a host’s model writes Cypher instead of jaala’s syntax (#354, and front ends on the plain-language page). Its card lists the graph’s labels with their properties and its relationship types, one a line, and its instructions say what of Cypher compiles and what doesn’t. Its examples are questions with the Cypher answering them, which Translate compiles and checks before it asks the model anything.
cypher.Grammar(graph) holds a model to the subset as GBNF, which llama.cpp decodes under: MATCH, WHERE, WITH, UNION, EXISTS { }, and RETURN with its aggregates, ORDER BY, SKIP and LIMIT. Its only names are the graph’s labels, types and properties. It leaves out CALL, $parameters and everything refused below. Every text it accepts parses. Whether the query means something, such as returning a variable it matched, is still Compile’s to say, as a program’s is Validate’s.
With nl.Options.Actions, the front end is an nl.Prober (#393, looking before answering): look Package describes a label or a relationship type, and values Package.license lists a property’s values through a Cypher query of its own.
Each refusal goes back as Explain writes it, a mistyped label with the label it likely meant, and a construct jaala can’t express yet as the kind gap, so a report counts which gaps real questions hit. The last example above is a model mistyping a label and fixing it on its second try.
Errors in the query’s terms ¶
A query can compile and still fail when the program checks or runs: a value an argument’s type or closed domain refuses ({tier: 'middle'}), or a number compared with text (p.loc > 'big'). The engine reports those against the program, so Program.Locate(err) maps one back to the Cypher that wrote the failing piece: the node pattern, the relationship, the WHERE condition, the property or the RETURN item (#353). The compiler records where each piece of the program came from as it writes it, and the engine says which argument or comparison a type error is about, so a refused value in the second of two patterns is placed at that pattern, not at the first. cypher.Explain(query, program, err) writes any error, a compile refusal included, the way a model or an agent fixes the query from: the message with the program’s variables named as the query names them (p.loc, not ?p_loc), the query’s line with a caret under the place, the fix when the engine has one, and for a gap, that the query should do without it, as the last example prints. An error with no place, such as a spent budget, is explained without a caret.