Skip to content

DECIMAL values between -1 and 1 cast to 0 when LC_NUMERIC locale uses comma as decimal separator #1428

@wolpos

Description

@wolpos

Environment

  • mysql2 version: 0.5.7
  • Ruby version: 3.4.8

Summary

When LC_NUMERIC is set to a locale that uses a comma as the decimal separator
(e.g. de_DE, fr_FR), DECIMAL values between -1 and 1 exclusive are incorrectly
cast to 0 instead of their actual value.

Values outside this range (e.g. 1.5, -2.3) are returned correctly.

Steps to reproduce

require 'fiddle'

libc = Fiddle.dlopen(nil)
setlocale = Fiddle::Function.new(
  libc['setlocale'],
  [Fiddle::TYPE_INT, Fiddle::TYPE_VOIDP],
  Fiddle::TYPE_VOIDP
)
setlocale.call(1, "de_DE.UTF-8")  # LC_NUMERIC = 1

client = Mysql2::Client.new(host: "localhost", username: "root")

client.query("SELECT CAST(0.5  AS DECIMAL(10,2)) AS val").first["val"]  # => 0.0  (wrong)
client.query("SELECT CAST(0.15 AS DECIMAL(10,2)) AS val").first["val"]  # => 0.0  (wrong)
client.query("SELECT CAST(1.5  AS DECIMAL(10,2)) AS val").first["val"]  # => 1.5  (correct)

Expected behavior

All DECIMAL values should be returned with their correct value regardless of
the process locale. CAST(0.5 AS DECIMAL(10,2)) should return BigDecimal("0.5").

Actual behavior

CAST(0.5 AS DECIMAL(10,2)) returns BigDecimal("0.0").

Only values whose integer part is 0 are affected (i.e. -1 < x < 1).

Root cause

In ext/mysql2/result.c, the MYSQL_TYPE_NEWDECIMAL branch in
rb_mysql_result_fetch_row uses strtod() to check whether the value is zero:

case MYSQL_TYPE_NEWDECIMAL:
  if (fields[i].decimals == 0) {
    val = rb_cstr2inum(row[i], 10);
  } else if (strtod(row[i], NULL) == 0.000000) {
    val = rb_funcall(rb_mKernel, intern_BigDecimal, 1, opt_decimal_zero);
  } else {
    val = rb_funcall(rb_mKernel, intern_BigDecimal, 1, rb_str_new(row[i], fieldLengths[i]));
  }
  break;

strtod() is locale-sensitive. With LC_NUMERIC=de_DE, it does not recognize
. as a decimal separator, so strtod("0.5") returns 0.0. This triggers the
zero-branch, and opt_decimal_zero ("0.0") is passed to BigDecimal() instead
of the actual string "0.5".

Values like "1.5" are unaffected because strtod("1.5") returns 1.0 in
any locale (the integer part is read correctly), so the zero-check is false and
the raw string "1.5" is passed to BigDecimal(), which is locale-independent.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions