Skip to content

TRAVERSE and SELECT from a bound RID collection throw NullPointerException #5505

Closed

Description

@robfrank

Problem

Passing a collection of RIDs as a query parameter target throws NullPointerException. Both of these
fail:

SELECT @rid AS rid FROM :seeds
SELECT @rid AS rid FROM (TRAVERSE out('LINK') FROM :seeds MAXDEPTH 2)

with params = {"seeds": List<RID>}:

java.lang.NullPointerException: Cannot invoke
"com.arcadedb.query.sql.parser.Expression.execute(com.arcadedb.query.sql.executor.Result,
 com.arcadedb.query.sql.executor.CommandContext)" because "this.expression" is null
  at com.arcadedb.query.sql.parser.Rid.toRecordId(Rid.java:71)
  at com.arcadedb.query.sql.executor.TraverseExecutionPlanner.handleRidsAsTarget(TraverseExecutionPlanner.java:220)
  at com.arcadedb.query.sql.executor.TraverseExecutionPlanner.handleInputParamAsTarget(TraverseExecutionPlanner.java:174)

A single bound RID works. Only a collection fails, which is what makes this easy to miss.

Cause

Both planners build a Rid AST node per element and set bucket and position, but never call
rid.setLegacy(true):

  • engine/src/main/java/com/arcadedb/query/sql/executor/TraverseExecutionPlanner.java:155-174
  • engine/src/main/java/com/arcadedb/query/sql/executor/SelectExecutionPlanner.java:1435-1453

Their own singleton-RID branches do call it, a few lines above in the same method:

  • TraverseExecutionPlanner.java:150
  • SelectExecutionPlanner.java:1425

Rid.toRecordId (engine/src/main/java/com/arcadedb/query/sql/parser/Rid.java:67-71) reads
bucket/position when legacy is true, and otherwise dereferences expression, which the
collection branch never populates:

  public RID toRecordId(final Result target, final CommandContext context) {
    if (legacy) {
      return context.getDatabase().newRID(bucket.value.intValue(), position.value.longValue());
    } else {
      final Object result = expression.execute(target, context);   // expression == null here

Note that Rid's two constructors differ: Rid(RID) sets legacy = true, while Rid(int) - the one
both collection branches use - leaves it false.

Fix

Add rid.setLegacy(true) to both collection branches, matching the singleton branches beside them.

Regression test

One test per planner, each binding a collection - a single-element bound RID passes today and
would not catch the regression:

final Map<String, Object> params = Map.of("seeds", List.of(v0.getIdentity(), v1.getIdentity()));
database.query("sql", "SELECT FROM :seeds", params);
database.query("sql", "SELECT FROM (TRAVERSE out('LINK') FROM :seeds MAXDEPTH 1)", params);

Impact and workaround

Any caller computing a set of RIDs and feeding it to a traversal or select must inline the RIDs as
literals (FROM [#1:0,#1:1]) instead of binding them. That form works and returns full BFS metadata
including $depth and $path.

Context

Found while designing the MCP hybrid_search tool (#4861), whose graph expansion leg traverses from a
computed seed set. That tool works around it by inlining RID literals - safe there because the seed
RIDs originate from the engine's own retrieval legs and never from caller text - and does not depend
on this fix landing.

Activity

  1. added a commit that references this issue on Jul 28, 2026
  2. added this to the 26.8.1 milestone on Jul 29, 2026
  3. added 3 commits that reference this issue on Aug 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions