vitessio/vitess · error

VT13001

VT13001

Error message

joins can only compare columns: %s

What it means

VT13001 panic in OuterExpressionsNeeded: when planning a join containing a subquery, the planner expects every join column expression on the left-hand side to be a plain column reference. If the LHS of a join comparison is a more complex expression, this internal invariant is violated and the planner panics rather than producing a wrong plan.

Source

Thrown at go/vt/vtgate/planbuilder/operators/subquery.go:84

	for _, jc := range columns {
		for _, lhsExpr := range jc.LHSExprs {
			offset := sq.Outer.AddColumn(ctx, true, false, aeWrap(lhsExpr.Expr))
			sq.Vars[lhsExpr.Name] = offset
		}
	}
	return nil
}

func (sq *SubQuery) OuterExpressionsNeeded(ctx *plancontext.PlanningContext, outer Operator) (result []*sqlparser.ColName) {
	joinColumns, err := sq.GetJoinColumns(ctx, outer)
	if err != nil {
		return nil
	}
	for _, jc := range joinColumns {
		for _, lhsExpr := range jc.LHSExprs {
			col, ok := lhsExpr.Expr.(*sqlparser.ColName)
			if !ok {
				panic(vterrors.VT13001("joins can only compare columns: %s", sqlparser.String(lhsExpr.Expr)))
			}
			result = append(result, col)
		}
	}
	return result
}

func (sq *SubQuery) GetJoinColumns(ctx *plancontext.PlanningContext, outer Operator) ([]applyJoinColumn, error) {
	if outer == nil {
		return nil, vterrors.VT13001("outer operator cannot be nil")
	}
	outerID := TableID(outer)
	if sq.JoinColumns != nil {
		if sq.outerID == outerID {
			return sq.JoinColumns, nil
		}
	}
	sq.outerID = outerID

View on GitHub (pinned to 01a25a7d17)

Solutions

  1. Rewrite the predicate so both sides are plain column comparisons (e.g. compare t.id directly, or precompute the expression in a derived table)
  2. Reduce the expression to an equality on raw columns so the subquery can be merged
  3. If it looks like a valid query that should work, file a bug with the SQL and stack trace
  4. Simplify the query or execute the subquery separately in application code

Example fix

// before
SELECT * FROM t WHERE t.id + 1 = (SELECT max(id) FROM u)
// after
SELECT * FROM t WHERE t.id = (SELECT max(id) - 1 FROM u)
Defensive patterns

Strategy: validation

Validate before calling

// ensure subquery correlation predicates compare bare columns
// e.g. reject: t.id + 1 = (SELECT ...) ; accept: t.id = (SELECT ...)

Type guard

func isPlainColumn(e sqlparser.Expr) bool {
    _, ok := e.(*sqlparser.ColName)
    return ok
}

Try / catch

err := doQuery(q)
if err != nil && strings.Contains(err.Error(), "VT13001") && strings.Contains(err.Error(), "joins can only compare columns") {
    return doQuery(rewriteToColumnEquality(q))
}

Prevention

When it happens

Trigger: Planning a query where a mergeable join/subquery comparison uses a non-column expression on the left side, e.g. `WHERE t.id + 1 = (SELECT ...)` or any function/complex expression as a join column.

Common situations: Queries with computed expressions in join or subquery correlation predicates instead of bare column equality; often surfaced when the planner attempts an unexpected merge of a subquery.

Related errors


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