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, err

View on GitHub (pinned to 01a25a7d17)

Solutions

  1. Verify the query's row constructors have equal element counts on both sides
  2. Check the comparison expression's compile path ran its cardinality typecheck; fix the missing validation if bypassed
  3. In custom callers, assert len(left)==len(right) before invoking tuple comparison
  4. 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

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


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