vitessio/vitess · error · VitessError

VT13001

VT13001

Error message

VT13001: [BUG] should not see %T here

What it means

VT13001 internal-bug panic in planOffsets. Offsets are planned after horizons are merged; encountering a *Horizon operator during the offset-planning walk means a Horizon survived earlier phases when it should have been collapsed. It is an invariant violation indicating a planner bug, not user error.

Source

Thrown at go/vt/vtgate/planbuilder/operators/offset_planning.go:38

	"fmt"

	"vitess.io/vitess/go/vt/sqlparser"
	"vitess.io/vitess/go/vt/vterrors"
	"vitess.io/vitess/go/vt/vtgate/planbuilder/plancontext"
	"vitess.io/vitess/go/vt/vtgate/semantics"
)

type offsettable interface {
	Operator
	planOffsets(ctx *plancontext.PlanningContext) Operator
}

// planOffsets will walk the tree top down, adding offset information to columns in the tree for use in further optimization,
func planOffsets(ctx *plancontext.PlanningContext, root Operator) Operator {
	visitor := func(in Operator, _ semantics.TableSet, _ bool) (Operator, *ApplyResult) {
		switch op := in.(type) {
		case *Horizon:
			panic(vterrors.VT13001(fmt.Sprintf("should not see %T here", in)))
		case offsettable:
			newOp := op.planOffsets(ctx)
			if newOp == nil {
				newOp = op
			}

			if DebugOperatorTree {
				fmt.Println("Planned offsets for:")
				fmt.Println(ToTree(newOp))
			}

			if newOp == op {
				return newOp, nil
			} else {
				// We got a new operator from plan offsets. We should return that something has changed.
				return newOp, Rewrote("planning offsets introduced a new operator")
			}
		}

View on GitHub (pinned to 01a25a7d17)

Solutions

  1. Capture the failing SQL and stack trace and file a Vitess bug
  2. Rewrite the query (inline subqueries, simplify nested SELECTs) as a workaround
  3. Retry on the latest release where horizon-merging has been reworked

Example fix

// before
SELECT * FROM (SELECT * FROM (SELECT id FROM t) a) b
// after: flatten to a single-level query
SELECT id FROM t
Defensive patterns

Strategy: fallback

Validate before calling

null

Type guard

null

Try / catch

try {
  rows = await vtgate.execute(session, query, bindVars);
} catch (e) {
  if (String(e.message).includes('VT13001')) {
    log.error({query, err: e}, 'planner bug: horizon survived offset planning');
    // retry with simplified/flattened query
  }
  throw e;
}

Prevention

When it happens

Trigger: The offset-planning pass (planOffsets) visits a *Horizon node — i.e. a query whose Horizon was not merged/lowered before offset planning runs (subqueries/nested SELECTs hitting an unhandled path).

Common situations: Queries with nested subqueries or layered projections that keep a Horizon alive; regressions after planner upgrades.

Related errors


AI-assisted analysis of vitessio/vitess@01a25a7d17 (2026-09-01). Data as JSON: /api/errors/1ec765e795fea207. Report an issue: GitHub.