Query: Question Classes and Idiom Cookbook

The query verb compiles GQL (Graph Query Language) and SPARQL strings to SQL SELECT statements over the knowledge graph (ADR-008). It is a structural pattern matcher, not a full-text or semantic search engine, and it is read-only: mutations always go through create, update, link, merge, and delete.

This guide has two parts: a catalogue of question classes mapped to the right verb (query, or something else), and a cookbook of verified idioms with copy-paste syntax and known gaps.

Routing: query vs. search vs. neighbors vs. traverse vs. context

query is one of five ways to read the graph. Reach for it only when the question is genuinely structural — a typed relation pattern, a property filter, or a bounded multi-hop path with a known relation. For everything else, a different verb is a better fit:

You want to… Use
Find records by topic or keyword, fuzzy or full-text search(kind="entity", query="...")kind="note" works the same way
Look up a known record by id get(id="...")
See immediate connections of a known node neighbors(node_id="...", direction="both")
Explore bounded multi-hop paths traverse(roots=["..."], max_depth=N) — depth ≤10 with finite result/work/time budgets
Pull a budgeted, entity-anchored context bundle context(query="...", hops=N, budget=N) — or anchor with entity_ids=[...] instead of query
Match a typed relation pattern, filter by property, or walk a fixed-relation bounded path query(query="MATCH ...") or query(query="SELECT ...")
Create, update, link, merge, or delete anything The corresponding KG verb — never query

Rule of thumb: if the answer depends on relevance ranking or you don’t know the exact relation name, use search. If you already hold an id and want its neighborhood, use neighbors or traverse. Reach for query only once you can name the relation(s) and shape of the pattern you’re matching.

Question-class catalogue

Verdicts derived from the shipped parsers, AST validation, SQL compiler, the KG verb handlers, ADR-008, and live read-only checks against a production knowledge graph.

Question class Example question Verdict Idiom or boundary
Exact named-record lookup Does this canonical name already exist? YES via query MATCH (n) WHERE n.name = "<exact name>" RETURN n.id, n.kind, n.name LIMIT 2
Fuzzy discovery What records concern recursive CTE traversal? YES via search query has no full-text or semantic operator; use search(kind="entity", query="...").
Immediate adjacency What directly annotates this known record? YES via neighbors neighbors(node_id="<uuid>", direction="incoming", relations=["annotates"])
Targeted relation join Which concepts were introduced by which documents? YES via query MATCH (c:concept)-[e:introduced_by]->(d:document) RETURN c.name, d.name, e.weight LIMIT 50
Provenance path What is the derivation path from an artifact to its source? PARTIAL Fixed derived_from chains, or a single variable-length edge, both work. A chain that mixes fixed and variable-length hops must be split into separate queries.
Supersession view What replaces this concept? YES via query MATCH (old)-[e:supersedes]->(new) RETURN old.name, new.name, e.weight LIMIT 100
Relation alternatives Show records that extend or implement another record. YES via query Pipe-separated relation alternation inside one edge pattern — see the fenced example below the table (a literal pipe cannot be shown inside this table cell).
Bounded lineage What is reachable by one to three extends hops? YES via query MATCH (a)-[:extends*1..3]->(b) RETURN a.name, b.name LIMIT 100
General expansion Expand this known entity two hops with context. YES via traverse/context traverse(roots=["<uuid>"], max_depth=2), or context(entity_ids=["<uuid>"], hops=2, budget=N).
Cross-substrate join Which notes annotate concepts, documents, or other notes? YES via query MATCH (note:note)-[e:annotates]->(target) RETURN note.name, target.kind, target.name LIMIT 100
Pack-note inventory Which caller-visible comm messages exist? YES via query MATCH (m:message) RETURN m.id, m.kind LIMIT 100; equivalently, use MATCH (m:note) WHERE m.kind = "message" RETURN m.id, m.kind LIMIT 100.
Kind/type filtering Which records use a given entity type? YES via query MATCH (n) WHERE n.entity_type IS NOT NULL RETURN n.entity_type, n.name LIMIT 100
Property filtering Which names contain a token, or belong to a set of kinds? YES via query WHERE n.name CONTAINS "<text>", STARTS WITH, or n.kind IN ["concept", "project"].
Edge-weight filter Which extends edges have weight at least 0.8? YES via query MATCH (a)-[e:extends]->(b) WHERE e.weight >= 0.8 RETURN a.name, e.weight, b.name LIMIT 100
Deduplication candidates Which pairs share a name or a similar description? PARTIAL A literal-name lookup works. Variable-to-variable comparison, grouping, and similarity scoring do not — use search to surface candidates instead.
Orphan detection Which records have no graph edges? NO No negative pattern, NOT EXISTS, optional match, or anti-join exists. Approximate outside query with list plus neighbors.
Edge-density audit Which entities have fewer than four edges? PARTIAL stats() gives a global edges_by_relation breakdown and query lists rows, but COUNT, GROUP BY, HAVING, and degree projection are all absent.
Aggregate inventory How many concepts exist, by type? PARTIAL Filtered row listing and GQL SKIP paging work. Aggregate functions, grouping, distinct projection, and caller-defined ordering are absent.
Cycle / self-reachability Which entities are in an extends cycle? NO Repeated node-variable bindings are rejected, so cycles and self-reachability cannot be expressed.
Branching join Which parent has both an extends child and an implements child? NO GQL supports one alternating path; SPARQL rejects branched or disconnected WHERE blocks.
Cross-namespace audit What belongs to another namespace? NO (caller-controlled scope) Namespace is a runtime CompileOptions input, not query text — a query that references namespace in its body is rejected.
Mutation via query Create, update, link, merge, or delete through GQL/SPARQL. NO (by design) Write-shaped input is rejected outright. Use the corresponding KG verb.

The relation-alternatives idiom, copy-paste form (alternation is a bare pipe between relation identifiers inside one edge pattern):

MATCH (a)-[e:extends|implements]->(b) RETURN a.name, e.relation, b.name LIMIT 100

Operational limits

  • GQL accepts one connected, alternating path after MATCH. It does not accept comma-separated patterns or branching self-joins. For a parallel-edge audit, return a.id, e.id, e.relation, b.id from one unlabeled-edge path and group endpoint IDs client-side without discarding the edge ID.
  • RETURN accepts comma-separated variables or variable.property items, but not AS aliases. Output names remain variable-prefixed, such as a_id.
  • GQL supports either a chain of fixed-relation hops or exactly one variable-length edge per query — never both mixed in the same pattern. Minimum hop count is 1; maximum is 10. A bare * (no bounds) means 1 through 5 hops, not zero-or-more.
  • A query that mixes a fixed hop with a variable-length hop in the same chain is rejected outright, not silently approximated. Split it into two queries.
  • The query verb’s page_size has minimum 1, defaults to 500, and is clamped to a hard cap of 10,000. GQL SKIP advances through deterministically ordered matches. Omit query-text LIMIT for an exhaustive audit; a LIMIT at or below page_size is a terminal caller-chosen bound. The legacy outer limit argument remains a deprecated, mutually exclusive alias for page_size.
  • Result columns are projection-derived and variable-prefixed (for example a_name, e_relation). Returning a whole node variable expands it into its supported substrate columns. Variable-length paths add _depth and _total_weight (a running sum of edge weights along the path, not an average).
  • Pack-registered note kinds use the same GQL kind field as base kinds. A granular label such as :message is shorthand for the stored kind; :note selects the note substrate and can be combined with WHERE m.kind = "message". note_kind belongs to the verb parameter surface and is not a GQL field. Both forms remain restricted to the caller’s authorized namespace scopes.
  • SPARQL variables project as whole nodes (full column expansion), unlike GQL’s dotted per-field projection. SPARQL OFFSET is not supported.

Paging broad GQL results

The response always includes offset, effective page_size, has_more, and the compatibility alias truncated. A non-terminal GQL page also includes next_offset; set the next query’s SKIP to that exact value and keep the same page_size:

query(query="MATCH (a)-[r:depends_on]->(b) RETURN a.id, r.id, b.id", page_size=500)
# ... "has_more": true, "next_offset": 500

query(query="MATCH (a)-[r:depends_on]->(b) RETURN a.id, r.id, b.id SKIP 500", page_size=500)

next_offset advances by the number of rows actually emitted. It is omitted when has_more is false. The compiler orders fixed matches by their complete bound identities (including synthetic observation primary keys); recursive DISTINCT rows are ordered by depth, weight, and their complete projected value tuple. Repeated reads of unchanged data are therefore stable. This is offset paging, not a cross-call database snapshot: inserts or deletes that match the query can shift later pages.

Verified GQL idioms

Each row was issued as query(query="...") through the request tool against a production knowledge graph. Result shapes are column names and row counts only — no graph content is reproduced here.

# What it answers Query string Result shape / pitfall
1 Sample concept records MATCH (n:concept) RETURN n LIMIT 2 2 rows; columns n_created_at, n_entity_type, n_id, n_kind, n_name, n_namespace, n_properties, n_updated_at. A whole-node variable expands to every supported column.
2 Provenance endpoints MATCH (a)-[e:introduced_by]->(b) RETURN a.name, e.relation, b.name LIMIT 2 2 rows; a_name, b_name, e_relation.
3 Either extends or implements edge The alternation idiom in the fenced block above, with LIMIT 2 2 rows; a_name, b_name, e_relation. Alternation uses a pipe inside one edge pattern.
4 Incoming extends edge MATCH (a)<-[e:extends]-(b) RETURN a.name, e.weight, b.name LIMIT 2 2 rows; a_name, b_name, e_weight.
5 Undirected extends adjacency MATCH (a)-[e:extends]-(b) RETURN a.name, e.relation, b.name LIMIT 2 2 rows; a_name, b_name, e_relation. May expose both orientations.
6 One- to two-hop lineage MATCH (a)-[e:extends*1..2]->(b) RETURN a.name, b.name LIMIT 2 2 rows; _depth, _total_weight, a_name, b_name. Bounds are inclusive; maximum depth is 10.
7 Default variable-length lineage MATCH (a)-[e:extends*]->(b) RETURN a.name, b.name LIMIT 2 2 rows; _depth, _total_weight, a_name, b_name. Bare * means 1 through 5 hops, not zero-or-more.
8 Weight threshold MATCH (a)-[e:extends]->(b) WHERE e.weight >= 0 RETURN a.name, e.weight, b.name LIMIT 2 2 rows; a_name, b_name, e_weight. Edge predicates support relation and weight.
9 Kind equality filter MATCH (n) WHERE n.kind = "concept" RETURN n.name, n.kind LIMIT 2 2 rows; n_kind, n_name. Escape inner literal quotes in the outer DSL string.
10 Case-insensitive substring MATCH (n) WHERE n.name CONTAINS "ADR" RETURN n.name LIMIT 2 2 rows; n_name. CONTAINS escapes literal %, _, and \.
11 Prefix lookup MATCH (n) WHERE n.name STARTS WITH "ADR" RETURN n.name LIMIT 2 2 rows; n_name.
12 Multi-value filter MATCH (n) WHERE n.kind IN ["concept", "project"] RETURN n.name, n.kind LIMIT 2 Live-verified: 2 rows; n_kind, n_name. IN requires a scalar list; null is illegal.
13 Indexed entity-type filter MATCH (n) WHERE n.entity_type IS NOT NULL RETURN n.entity_type LIMIT 2 2 rows; n_entity_type. IS NOT NULL takes no right-hand literal.
14 WHERE ... OR ... MATCH (n) WHERE n.kind = "concept" OR n.kind = "project" RETURN n.name, n.kind LIMIT 2 2 rows; n_kind, n_name. AND binds tighter than OR; parentheses are not available to force grouping.
15 Page size vs. query LIMIT query(query="MATCH (n) RETURN n.name LIMIT 5", page_size=2) 2 rows plus has_more: true and next_offset: 2; query LIMIT exceeds the effective page size, so the +1 sentinel detects another page.

Parser-diagnosed dialect boundaries

These unsupported constructs receive specific parser errors rather than generic grammar or trailing-input failures:

Form Query string Contract and alternative
Comma-separated patterns MATCH (a)-[e1]->(b), (a)-[e2]->(b) RETURN a, b QueryError::Unsupported names comma-separated MATCH patterns. For parallel-edge audits, use one path returning a.id, e.id, e.relation, b.id, then group endpoint IDs client-side while retaining each edge ID.
RETURN aliases with AS MATCH (a)-[e]->(b) RETURN a.name AS src, b.name AS dst LIMIT 5 QueryError::Unsupported names AS. Use unaliased variable or variable.property projections; result names remain variable-prefixed.

Rejected forms (live-checked)

Each is a verified read-only check against the running server. Errors are verbatim.

Form Query string Observed error
GQL write CREATE (n:concept) query: unsupported feature: the query verb is read-only; to mutate the graph use: create, update, link, merge, delete
SPARQL update INSERT DATA { ?a :extends ?b } same read-only rejection as above
Mixed fixed and variable chain MATCH (a)-[e:extends]->(b)-[f:extends*1..2]->(c) RETURN a, b, c query: unsupported feature: variable-length patterns must be a single start_node -[*N..M]-> end_node (mixed fixed/variable chains are not yet implemented)
Zero-hop GQL range MATCH (a)-[e:extends*0..2]->(b) RETURN a, b query: unsupported feature: zero-hop ranges (min_hops = 0) not yet supported; use a minimum of 1 hop
Unknown relation MATCH (a)-[e:bogus]->(b) RETURN a, b query: validation error: unknown edge_relation: "bogus" (lists the 17 valid relations)
Repeated binding / cycle MATCH (a)-[e:extends]->(a) RETURN a query: unsupported feature: repeated node variable 'a' (cycle / self-reachability requires alias-equality predicates not yet implemented)
Branched SPARQL SELECT ?a ?b ?c WHERE { ?a :extends ?b . ?a :implements ?c . } query: unsupported feature: SPARQL WHERE block is branched or disconnected; only single-path patterns are supported
SPARQL zero-or-more SELECT ?a WHERE { ?a :extends* ?b . } query: unsupported feature: SPARQL '*' (zero-or-more hops) not yet supported; use '+' or '{min,max}'
Namespace from query text MATCH (n) WHERE n.namespace = "local" RETURN n LIMIT 1 query: validation error: namespace is set by CompileOptions, not query text
Incomplete float literal MATCH (n) WHERE n.name = 1. RETURN n LIMIT 1 query: parse error: float literal must have digits after '.' (e.g. '1.0', not '1.')

SPARQL

SPARQL positive-path execution is verified end to end: a one-hop SELECT returns rows on the live MCP tool.

What it answers Query string Notes
Which endpoints have an extends edge SELECT ?a ?b WHERE { ?a :extends ?b . } LIMIT 2 2 rows. SPARQL variables project as whole nodes — full column expansion (a_id, a_kind, a_name, a_properties, ...), unlike GQL’s dotted projection.
Which concepts have an introduced_by source SELECT ?a ?b WHERE { ?a a :concept . ?a :introduced_by ?b . } LIMIT 2 Typed-node triple pattern (?a a :concept) combined with a relation triple.
Which endpoints are one or two extends hops apart SELECT ?a ?b WHERE { ?a :extends{1,2} ?b . } LIMIT 2 Bounded variable-length path using the {min,max} form.

Gaps

  • Comma-separated GQL MATCH patterns and RETURN ... AS ... aliases are rejected with named unsupported-feature errors. Use one alternating path and unaliased projections; group returned rows client-side where needed.
  • Mixed fixed-plus-variable-length paths are rejected; split the query into separate calls instead.
  • No anti-join, optional match, aggregate, GROUP BY, DISTINCT, or caller-defined ordering exists. GQL uses SKIP rather than an OFFSET keyword; SPARQL OFFSET remains unsupported. Orphan-detection and edge-density questions cannot be expressed in a single query.
  • A repeated node-variable binding is rejected, which blocks cycle and self-reachability queries.
  • SPARQL is single-path only; a branched WHERE block is rejected.

See also


Raw markdown for this page: /md/query-cookbook.md