vitessio/vitess · error
did not typecheck cardinality
Error message
did not typecheck cardinality
What it means
evalCompareTuplesNullSafe compares two row-value tuples element-wise and assumes the typechecker already guaranteed both tuples have the same cardinality. The panic fires when left and right tuple expressions have different lengths, meaning the earlier compile-time cardinality check was bypassed or buggy. It protects the index-based loop from out-of-range access.
Source
Thrown at go/vt/vtgate/evalengine/expr_compare.go:288
return compareNumeric(lf, rf)
}
}
// fallbackBinary compares two values of the same type using the fallback binary comparison.
// This is for types we don't yet properly support otherwise but do end up being used
// for comparisons, for example when using vdiff.
// TODO: Clean this up as we add more properly supported types and comparisons.
func fallbackBinary(t sqltypes.Type) bool {
switch t {
case sqltypes.Bit, sqltypes.Enum, sqltypes.Set, sqltypes.Geometry, sqltypes.Vector:
return true
}
return false
}
func evalCompareTuplesNullSafe(left, right []eval, collationEnv *collations.Environment) (int, error) {
if len(left) != len(right) {
panic("did not typecheck cardinality")
}
for idx, lResult := range left {
res, err := evalCompareNullSafe(lResult, right[idx], collationEnv)
if err != nil {
return 0, err
}
if res != 0 {
return res, nil
}
}
return 0, nil
}
// eval implements the expression interface
func (c *ComparisonExpr) eval(env *ExpressionEnv) (eval, error) {
left, err := c.Left.eval(env)
if err != nil {
return nil, errView on GitHub (pinned to 01a25a7d17)
Solutions
- Verify the query's row constructors have equal element counts on both sides
- Check the comparison expression's compile path ran its cardinality typecheck; fix the missing validation if bypassed
- In custom callers, assert len(left)==len(right) before invoking tuple comparison
- File a Vitess bug with the query if produced by normal SQL
Example fix
// before
res, err := evalCompareTuplesNullSafe(l, r, env.CollationEnv())
// after
if len(l) != len(r) {
return nil, vterrors.Errorf(vtrpcpb.Code_INTERNAL, "tuple cardinality mismatch: %d vs %d", len(l), len(r))
}
res, err := evalCompareTuplesNullSafe(l, r, env.CollationEnv()) Defensive patterns
Strategy: validation
Validate before calling
// Before issuing row-constructor comparisons, ensure equal arity on both sides
func tupleArityMatch(nLeft, nRight int) error {
if nLeft != nRight {
return fmt.Errorf("row constructor arity mismatch: %d vs %d", nLeft, nRight)
}
return nil
} Type guard
func sameTupleCardinality(l, r evalengine.TupleExpr) bool {
return len(l.Values) == len(r.Values)
} Try / catch
func safeCompare(l, r []evalengine.eval, env *collations.Environment) (res int, err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("tuple compare panic: %v", r)
}
}()
return evalengine.EvalCompareTuplesNullSafe(l, r, env)
} Prevention
- Write row constructors with matching element counts on both sides
- Keep planbuilder's tuple cardinality typecheck intact and covered by tests
- Avoid hand-building tuple comparisons without arity validation
- Add compile-time assertions in tests for IN/row-constructor planning
When it happens
Trigger: Evaluating a null-safe tuple comparison (e.g., `(a,b) <=> (c,d)` or IN over row constructors) where the two tuple sides evaluated to different numbers of elements — possible only if compile-time validation of tuple lengths was skipped or produced mismatched expression trees.
Common situations: Queries with row constructors of unequal arity that reached evaluation due to a planbuilder bug; custom code constructing TupleExpr comparisons without arity validation; evalengine development/tests.
Related errors
- malformed hex literal from parser
- bad unsigned integer type
- bad type aggregation for signed/unsigned types
- unreachable
- unhandled case: evalIsTruthy
AI-assisted analysis of vitessio/vitess@01a25a7d17 (2026-09-01).
Data as JSON: /api/errors/3332c83664a99a4a.
Report an issue: GitHub.