{"record":{"id":"196b0f2599d3b6f1","repo":"vitessio/vitess","slug":"can-t-convert-s-to-decimal-too-many-s","errorCode":null,"errorMessage":"can't convert %s to decimal: too many .s","messagePattern":"can't convert (.+?) to decimal: too many \\.s","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"go/mysql/decimal/scan.go","lineNumber":123,"sourceCode":"\t}\n\n\tif len(s) <= 18 {\n\t\tdec, err := parseDecimal64(s)\n\t\tif err == nil {\n\t\t\tif neg {\n\t\t\t\tdec.value.Neg(dec.value)\n\t\t\t}\n\t\t\treturn dec, nil\n\t\t}\n\t\tif err != errOverflow {\n\t\t\treturn Decimal{}, fmt.Errorf(\"can't convert %s to decimal: %v\", original, err)\n\t\t}\n\t}\n\n\tvar fractional, integral []byte\n\tif pIndex := bytes.IndexByte(s, '.'); pIndex >= 0 {\n\t\tif bytes.IndexByte(s[pIndex+1:], '.') != -1 {\n\t\t\treturn Decimal{}, fmt.Errorf(\"can't convert %s to decimal: too many .s\", original)\n\t\t}\n\t\tif pIndex+1 < len(s) {\n\t\t\tintegral = s[:pIndex]\n\t\t\tfractional = s[pIndex+1:]\n\t\t} else {\n\t\t\tintegral = s[:pIndex]\n\t\t}\n\t} else {\n\t\tintegral = s\n\t}\n\n\t// Check if the size of this bigint would fit in the limits\n\t// that MySQL has by default. To do that, we must convert the\n\t// length of our integral and fractional part to \"mysql digits\"\n\tmyintg := myBigDigits(int32(len(integral)))\n\tmyfrac := myBigDigits(int32(len(fractional)))\n\tif myintg > MyMaxBigDigits {\n\t\treturn largestForm(MyMaxPrecision, 0, neg), nil","sourceCodeStart":105,"sourceCodeEnd":141,"githubUrl":"https://github.com/vitessio/vitess/blob/01a25a7d176f94613b8d59d799f438380a8760e4/go/mysql/decimal/scan.go#L105-L141","documentation":"Returned by decimal.Scan conversion helpers when the input string contains more than one '.' character, so it cannot be interpreted as a decimal literal.","triggerScenarios":"Calling NewFromMySQL with a payload like \"1.2.3\" — bytes.IndexByte finds the first dot, then a second dot in the remainder triggers the rejection.","commonSituations":"Concatenation bugs when building decimal strings (e.g. joining integral and fractional parts that already contain dots); corrupted wire data; callers passing formatted output like \"1.23.45\" from other systems.","solutions":["Inspect the quoted string in the error and fix the producer so only one dot is emitted.","If joining integral/fractional parts programmatically, trim dots from the parts before concatenation.","Validate the payload with a single-dot check before calling NewFromMySQL to fail earlier with better context."],"exampleFix":"// before\npayload := integral + \".\" + fractional // fractional = \"2.3\"\nNewFromMySQL([]byte(payload))\n\n// after\nfractional = strings.ReplaceAll(fractional, \".\", \"\")\npayload := integral + \".\" + fractional","handlingStrategy":"validation","validationCode":"func hasSingleDot(s []byte) bool {\n    return bytes.Count(s, []byte(\".\")) <= 1\n}\n// before calling:\n// if !hasSingleDot(data) { return error }","typeGuard":null,"tryCatchPattern":"dec, err := decimal.NewFromMySQL(data)\nif err != nil {\n    if strings.Contains(err.Error(), \"too many .s\") {\n        return vterrors.Errorf(vtrpcpb.Code_INVALID_ARGUMENT, \"malformed decimal %q\", data)\n    }\n    return vterrors.Wrapf(err, \"decimal conversion failed\")\n}","preventionTips":["When composing decimals from parts, ensure the parts contain no dots of their own.","Validate payloads with a single-dot check before conversion.","Avoid string concatenation for decimal construction; use the parser's native parts API when available.","Log the offending string — it identifies the buggy producer immediately."],"tags":["mysql","decimal","parsing","malformed-input"],"backgroundTag":"invalid-decimal-string","analyzedSha":"01a25a7d176f94613b8d59d799f438380a8760e4","analyzedAt":"2026-09-01T17:28:30.605Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}