vitessio/vitess · error

unexpected character %q

Error message

unexpected character %q

What it means

parseDecimal64 parses short (<=18 digit) decimal strings for NewFromMySQL. It only accepts digits, a single dot, and a leading sign; any other character (letter, space, second sign, etc.) aborts parsing with this error naming the offending character.

Source

Thrown at go/mysql/decimal/scan.go:50

func parseDecimal64(s []byte) (Decimal, error) {
	const cutoff = math.MaxUint64/10 + 1
	var n uint64
	dot := -1

	for i, c := range s {
		var d byte
		switch {
		case c == '.':
			if dot > -1 {
				return Decimal{}, errors.New("too many .s")
			}
			dot = i
			continue
		case '0' <= c && c <= '9':
			d = c - '0'
		default:
			return Decimal{}, fmt.Errorf("unexpected character %q", c)
		}

		if n >= cutoff {
			// n*base overflows
			return Decimal{}, errOverflow
		}
		n *= 10
		n1 := n + uint64(d)
		if n1 < n {
			return Decimal{}, errOverflow
		}
		n = n1
	}

	var exp int32
	if dot != -1 {
		exp = -int32(len(s) - dot - 1)
	}

View on GitHub (pinned to 01a25a7d17)

Solutions

  1. Inspect the input string (it is quoted in the error) and strip or fix the invalid character.
  2. Ensure the bytes come from MySQL's binary DECIMAL wire encoding or a clean decimal literal — no spaces, commas, or exponent notation.
  3. If parsing user text, convert with a lenient parser (strings.TrimSpace, remove separators) before calling NewFromMySQL.
  4. Verify the upstream sender/proxy is not mangling the decimal payload.

Example fix

// before
NewFromMySQL([]byte("1 234.5"))

// after
clean := strings.Map(func(r rune) rune { if r == ' ' { return -1 }; return r }, "1 234.5")
NewFromMySQL([]byte(clean))
Defensive patterns

Strategy: validation

Validate before calling

func isCleanDecimal(b []byte) bool {
    s := b
    if len(s) > 0 && (s[0] == '+' || s[0] == '-') {
        s = s[1:]
    }
    seenDot := false
    for _, c := range s {
        if c == '.' {
            if seenDot {
                return false
            }
            seenDot = true
            continue
        }
        if c < '0' || c > '9' {
            return false
        }
    }
    return len(s) > 0
}

Try / catch

dec, err := decimal.NewFromMySQL(data)
if err != nil {
    return vterrors.Wrapf(err, "cannot parse decimal payload %q", data)
}

Prevention

When it happens

Trigger: NewFromMySQL receives a wire-format MySQL binary decimal string containing a character outside [0-9.] (after optional leading '-'/'+' handled by the caller) while the string is <=18 chars, so it routes into parseDecimal64.

Common situations: Corrupted or non-standard wire data from a misbehaving proxy or older/incompatible MySQL server; a caller passing a human-formatted string (with spaces, thousands separators, or exponent notation) directly to NewFromMySQL instead of clean binary decimal bytes.

Related errors


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