cayleygraph/cayley · error

iterator already used in query

Error message

iterator already used in query

What it means

path/iteratorShape wraps a shape.Shape so its BuildIterator can only be called once: after the first call it nils out the stored shape and sets sent=true. A second call returns iterator.NewError with 'iterator already used in query'. This guards against reusing a single iterator instance across queries, which would corrupt iteration state since shapes are consumed when built.

Source

Thrown at query/path/morphism_apply_functions.go:252

func savePredicatesMorphism(isIn bool, tag string) morphism {
	return morphism{
		Reversal: func(ctx *pathContext) (morphism, *pathContext) {
			return savePredicatesMorphism(isIn, tag), ctx
		},
		Apply: func(in shape.Shape, ctx *pathContext) (shape.Shape, *pathContext) {
			return shape.SavePredicates(in, isIn, tag), ctx
		},
	}
}

type iteratorShape struct {
	it   iterator.Shape
	sent bool
}

func (s *iteratorShape) BuildIterator(qs graph.QuadStore) iterator.Shape {
	if s.sent {
		return iterator.NewError(fmt.Errorf("iterator already used in query"))
	}
	it := s.it
	s.it, s.sent = nil, true
	return it
}
func (s *iteratorShape) Optimize(ctx context.Context, r shape.Optimizer) (shape.Shape, bool) {
	return s, false
}

// iteratorMorphism simply tacks the input iterator onto the chain.
func iteratorMorphism(it iterator.Shape) morphism {
	return morphism{
		Reversal: func(ctx *pathContext) (morphism, *pathContext) { return iteratorMorphism(it), ctx },
		Apply: func(in shape.Shape, ctx *pathContext) (shape.Shape, *pathContext) {
			return join(&iteratorShape{it: it}, in), ctx
		},
	}
}

View on GitHub (pinned to 81dcd7d73e)

Solutions

  1. Rebuild the path/query (call the step's BuildPath / query BuildIteratorTree again) for each execution.
  2. Keep the query *specification* (steps) around and construct a fresh iterator per run, not the built iterator.
  3. Wrap query execution in a helper that always creates a new iterator shape.
  4. Check for accidental double invocation of BuildIterator on the same object in your code path.

Example fix

// before
it := p.BuildIterator(qs)
run(it)
run(p.BuildIterator(qs)) // second call: error iterator
// after
run(p.BuildIterator(qs))
run(p.BuildIterator(qs)) // path rebuilds a fresh shape each call
Defensive patterns

Strategy: fallback

Validate before calling

type onceIterator struct{ built atomic.Bool }
func guardReuse(build func() iterator.Shape) iterator.Shape {
  if !buildOnce.built.CompareAndSwap(false, true) {
    return build() // rebuild instead of reusing the consumed shape
  }
  return build()
}

Type guard

func isConsumed(s *path.IteratorShape) bool { return s == nil || s.Sent() }

Try / catch

it := p.BuildIterator(qs)
if errIt, ok := it.(*iterator.Error); ok {
  // e.g. 'iterator already used in query' — rebuild the query
  it = p.BuildIterator(qs)
}

Prevention

When it happens

Trigger: Calling BuildIterator (or BuildIteratorTree) twice on the same path.Path / query object without rebuilding it, e.g. re-running a stored query object.

Common situations: Caching a *path.Path and executing it multiple times; reusing a query across requests in a server; running the same query variable twice in batch jobs.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


AI-assisted analysis of cayleygraph/cayley@81dcd7d73e (2026-09-06). Data as JSON: /api/errors/3a727c634104b7ac. Report an issue: GitHub.