Bug #64666 MySQL discards fractional part of the default values of system
Submitted: 15 Mar 2012 23:47 Modified: 16 Mar 2012 18:29
Reporter: Nizameddin Ordulu Email Updates:
Status: Verified Impact on me:
None 
Category:MySQL Server: Options Severity:S2 (Serious)
Version:5.1,5.5,5.6 OS:Any
Assigned to: CPU Architecture:Any
Tags: Contribution, global variables, system variables
Triage: Needs Triage: D2 (Serious)

[15 Mar 2012 23:47] Nizameddin Ordulu
Description:
MySQL ignores the fractional part of the default value for system variables of type double. This is because the struct that's used to represent options have a fields of type longlong to represent the min,max,default values. In my_getopt.h:

43struct my_option
44{
45  const char *name;                     /* Name of the option */
46  int        id;                        /* unique id or short option */
47  const char *comment;                  /* option comment, for autom. --help */
48  void       *value;                    /* The variable value */
49  void       *u_max_value;              /* The user def. max variable value */
50  struct st_typelib *typelib;           /* Pointer to possible values */
51  ulong      var_type;                  /* Must match the variable type */
52  enum get_opt_arg_type arg_type;
53  longlong   def_value;                 /* Default value */
54  longlong   min_value;                 /* Min allowed value */
55  longlong   max_value;                 /* Max allowed value */
56  longlong   sub_size;                  /* Subtract this from given value */
57  long       block_size;                /* Value should be a mult. of this */
58  void       *app_type;                 /* To be used by an application */
59};

I now truly understand why a lot of global vars in innodb are percentages instead of doubles: because doubles do not work. Fix is easy, either add double fields to my_opt, or have a precision (like 6 digits after decimal point) and store 10^n times the original value in the my_opt struct and later divide this value when assigning it back. I currently fixed it by multiplying the double by 1000000 to obtain the longlong and later dividing long long by 1000000 for global variables of type DOUBLE. Diff is included.

How to repeat:
Add a sysvar of type double with a default value that has nonzero fractional part like 1.5 or 0.7. Query for the value of this global variable in mysql client. The default value will be truncated to the nearest integer.

Suggested fix:
diff --git a/mysys/my_getopt.c b/mysys/my_getopt.c
index 07c7401..f048495 100644
--- a/mysys/my_getopt.c
+++ b/mysys/my_getopt.c
@@ -972,6 +972,8 @@ static double getopt_double(char *arg, const struct my_option *optp, int *err)
   double num;
   int error;
   char *end= arg + 1000;                     /* Big enough as *arg is \0 terminated */
+  double max_value = ((double)optp->max_value) / 1000000;
+  double min_value = ((double)optp->min_value) / 1000000;
   num= my_strtod(arg, &end, &error);
   if (end[0] != 0 || error)
   {
@@ -981,9 +983,9 @@ static double getopt_double(char *arg, const struct my_option *optp, int *err)
     *err= EXIT_ARGUMENT_INVALID;
     return 0.0;
   }
-  if (optp->max_value && num > (double) optp->max_value)
-    num= (double) optp->max_value;
-  return max(num, (double) optp->min_value);
+  if (max_value && num > max_value)
+    num= max_value;
+  return max(num, min_value);
 }
 
 double getopt_double_limit_value(double num, double max_val, double min_val,
@@ -1052,7 +1054,7 @@ static void init_one_value(const struct my_option *option, void *variable,
     *((ulonglong*) variable)= (ulonglong) value;
     break;
   case GET_DOUBLE:
-    *((double*) variable)=  (double) value;
+    *((double*) variable)=  ((double) value)/1000000;
     break;
   case GET_STR:
     /*
diff --git a/sql/sql_plugin.cc b/sql/sql_plugin.cc
index 6208bc8..a86a780 100644
--- a/sql/sql_plugin.cc
+++ b/sql/sql_plugin.cc
@@ -2890,9 +2890,15 @@ bool sys_var_pluginvar::update(THD *thd, set_var *var)
 
 #define OPTION_SET_LIMITS(type, options, opt) \
   options->var_type= type; \
-  options->def_value= (opt)->def_val; \
-  options->min_value= (opt)->min_val; \
-  options->max_value= (opt)->max_val; \
+  if (type != GET_DOUBLE) { \
+    options->def_value= (opt)->def_val; \
+    options->min_value = (opt)->min_val; \
+    options->max_value = (opt)->max_val; \
+  } else { \
+    options->def_value = 1000000 * (opt)->def_val; \
+    options->min_value = 1000000 * (opt)->min_val; \
+    options->max_value = 1000000 * (opt)->max_val; \
+  } \
   options->block_size= (long) (opt)->blk_sz
[16 Mar 2012 8:54] Valeriy Kravchuk
Thank you for the problem report and patch contributed.
[16 Mar 2012 18:29] Nizameddin Ordulu
Actuallly, if you are going to pick up this patch, can you change the constant to a power of two, rather than 10^6. My new diff is below:

diff --git a/mysys/my_getopt.c b/mysys/my_getopt.c
index 07c7401..3b11498 100644
--- a/mysys/my_getopt.c
+++ b/mysys/my_getopt.c
@@ -972,6 +972,8 @@ static double getopt_double(char *arg, const struct my_option *optp, int *err)
   double num;
   int error;
   char *end= arg + 1000;                     /* Big enough as *arg is \0 terminated */
+  double max_value = ((double)optp->max_value) / (1 << 20);
+  double min_value = ((double)optp->min_value) / (1 << 20);
   num= my_strtod(arg, &end, &error);
   if (end[0] != 0 || error)
   {
@@ -981,9 +983,9 @@ static double getopt_double(char *arg, const struct my_option *optp, int *err)
     *err= EXIT_ARGUMENT_INVALID;
     return 0.0;
   }
-  if (optp->max_value && num > (double) optp->max_value)
-    num= (double) optp->max_value;
-  return max(num, (double) optp->min_value);
+  if (max_value && num > max_value)
+    num= max_value;
+  return max(num, min_value);
 }
 
 double getopt_double_limit_value(double num, double max_val, double min_val,
@@ -1052,7 +1054,7 @@ static void init_one_value(const struct my_option *option, void *variable,
     *((ulonglong*) variable)= (ulonglong) value;
     break;
   case GET_DOUBLE:
-    *((double*) variable)=  (double) value;
+    *((double*) variable)=  ((double) value)/(1 << 20);
     break;
   case GET_STR:
     /*
diff --git a/sql/sql_plugin.cc b/sql/sql_plugin.cc
index 6208bc8..a3e3fe3 100644
--- a/sql/sql_plugin.cc
+++ b/sql/sql_plugin.cc
@@ -2890,9 +2890,15 @@ bool sys_var_pluginvar::update(THD *thd, set_var *var)
 
 #define OPTION_SET_LIMITS(type, options, opt) \
   options->var_type= type; \
-  options->def_value= (opt)->def_val; \
-  options->min_value= (opt)->min_val; \
-  options->max_value= (opt)->max_val; \
+  if (type != GET_DOUBLE) { \
+    options->def_value= (opt)->def_val; \
+    options->min_value = (opt)->min_val; \
+    options->max_value = (opt)->max_val; \
+  } else { \
+    options->def_value = (1 << 20) * (opt)->def_val; \
+    options->min_value = (1 << 20) * (opt)->min_val; \
+    options->max_value = (1 << 20) * (opt)->max_val; \
+  } \
   options->block_size= (long) (opt)->blk_sz