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 = outerIDView on GitHub (pinned to 01a25a7d17)
Solutions
- Rewrite the predicate so both sides are plain column comparisons (e.g. compare t.id directly, or precompute the expression in a derived table)
- Reduce the expression to an equality on raw columns so the subquery can be merged
- If it looks like a valid query that should work, file a bug with the SQL and stack trace
- 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
- Write subquery predicates as direct column-to-column equality
- Precompute expressions in a derived table before joining
- Test queries with expressions in join predicates against your Vitess version
- Avoid function calls on join columns in subquery correlation
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.