fix(query): make agent query errors actionable

What does this MR do and why?

When an agent sent a bad query, the Orbit error did not tell it how to fix the query. The agent guessed, and it sent the same mistake again. This MR makes syntax and schema errors actionable. The parser names an unsupported function such as type(), and it names clauses instead of grammar rules. The schema command rejects unknown node names. The traversal_path scope loop is fixed in the next MR in the stack, which hides that field from clients.

Relates to #1377. Companion MRs: gitlab-org/modelops/applied-ml/code-suggestions/ai-assist!7405 (Duo Chat card) and gitlab-org/gitlab!261313 (merged) (MCP client errors).

Testing

I tested end to end with Duo Chat on a GDK VM. The VM ran this MR, the ai-assist MR, and the gitlab MR together.

Before (gitlab.com) After (GDK)
before-gitlab-com-panel after-vm1-call-1
More screenshots

after-vm1-call-4

After this change, the agent proposed the correct fix for each of the four bad calls.

Performance Analysis

  • This merge request does not introduce any performance regression. If a performance regression is expected, explain why.

The new code runs only on error paths.

Agent context: long-form analysis, file-by-file walkthroughs, profiler output, alternatives considered

How it works

  1. A FunctionCall grammar rule matches any name(args) in RETURN, ORDER BY, and both sides of a WHERE comparison. The syntax tree consumer always rejects it, like LegacyShortestPath. The error names the function, which is an ASCII identifier and never a literal, and lists the supported functions. For type(r), it points to typed relationships. See query.pest#L29-33 and syntax.rs#L605-618.

    FunctionCall = { FunctionName ~ "(" ~ SP? ~ (FunctionArgument ~ (SP? ~ "," ~ SP? ~ FunctionArgument)*)? ~ SP? ~ ")" }
    FunctionName = @{ !ReservedWord ~ UnescapedSymbolicName }
  2. pest rule names in syntax errors map to clause names through renamed_rules, so expected EOI, Order, Limit, Page, or Debug becomes expected end of query, ORDER BY, LIMIT, PAGE, or DEBUG.

  3. get_graph_schema rejects expand_nodes names that are not in the ontology and lists the valid names. * still expands every node. The REST schema endpoint uses a different RPC, so it does not change. See service.rs#L244-273.

Error text before and after

Call Before After
RETURN m.id, type(r) ... column 74: expected EOI function type() is not supported: match a typed relationship such as -[r:CLOSES]->; each edge type is listed under @edges in the response
RETURN u LIMIT(1) expected EOI, Order, Limit, Page, or Debug expected end of query, ORDER BY, LIMIT, PAGE, or DEBUG
expand_nodes: ["NoSuchNode"] full schema, no error Invalid arguments: unknown node(s) in expand_nodes: NoSuchNode. Valid nodes: ...

Tests

  • New scenarios edge_cases/gql_unsupported_function.yaml, gql_unsupported_function_in_where.yaml (also checks that the literal is not echoed), gql_supported_function_with_unsupported_arguments.yaml, and gql_syntax_error_names_clauses.yaml.
  • graph_schema_ignores_unknown_expansion_nodes became graph_schema_rejects_unknown_expansion_nodes.
  • Local runs: compiler 353 passed, integration-tests --test local 225 passed, integration-testkit 135 passed, orbit-server graph_schema 10 passed. Both GQL fuzz targets pass. Clippy reported 0 warnings.
  • The literal check passed: errors never echo 'private-query-literal'.
  • Follow-up: type(r) support (WHERE type(r) = 'X' lowers to edge.types; RETURN type(r) as a no-op), then aggregation group by type(r) with security scenarios.
Edited by Michael Angelo Rivera

Merge request reports

Loading
Loading