]> git.ipfire.org Git - thirdparty/postgresql.git/commitdiff
Fix corner-case errors and loss of precision in numeric_power().
authorDean Rasheed <dean.a.rasheed@gmail.com>
Sat, 31 Jul 2021 10:28:10 +0000 (11:28 +0100)
committerDean Rasheed <dean.a.rasheed@gmail.com>
Sat, 31 Jul 2021 10:28:10 +0000 (11:28 +0100)
This fixes a couple of related problems that arise when raising
numbers to very large powers.

Firstly, when raising a negative number to a very large integer power,
the result should be well-defined, but the previous code would only
cope if the exponent was small enough to go through power_var_int().
Otherwise it would throw an internal error, attempting to take the
logarithm of a negative number. Fix this by adding suitable handling
to the general case in power_var() to cope with negative bases,
checking for integer powers there.

Next, when raising a (positive or negative) number whose absolute
value is slightly less than 1 to a very large power, the result should
approach zero as the power is increased. However, in some cases, for
sufficiently large powers, this would lose all precision and return 1
instead of 0. This was due to the way that the local_rscale was being
calculated for the final full-precision calculation:

  local_rscale = rscale + (int) val - ln_dweight + 8

The first two terms on the right hand side are meant to give the
number of significant digits required in the result ("val" being the
estimated result weight). However, this failed to account for the fact
that rscale is clipped to a maximum of NUMERIC_MAX_DISPLAY_SCALE
(1000), and the result weight might be less then -1000, causing their
sum to be negative, leading to a loss of precision. Fix this by
forcing the number of significant digits calculated to be nonnegative.
It's OK for it to be zero (when the result weight is less than -1000),
since the local_rscale value then includes a few extra digits to
ensure an accurate result.

Finally, add additional underflow checks to exp_var() and power_var(),
so that they consistently return zero for cases like this where the
result is indistinguishable from zero. Some paths through this code
already returned zero in such cases, but others were throwing overflow
errors.

Dean Rasheed, reviewed by Yugo Nagata.

Discussion: http://postgr.es/m/CAEZATCW6Dvq7+3wN3tt5jLj-FyOcUgT5xNoOqce5=6Su0bCR0w@mail.gmail.com

src/backend/utils/adt/numeric.c
src/test/regress/expected/numeric.out
src/test/regress/sql/numeric.sql

index 38feaeba06fa1b7c6e0844bc3fd1065f6601f31f..5761673b7f3f2cd2c3a603fcdca7d01970d9aa29 100644 (file)
@@ -3016,7 +3016,9 @@ numeric_power(PG_FUNCTION_ARGS)
        /*
         * The SQL spec requires that we emit a particular SQLSTATE error code for
         * certain error conditions.  Specifically, we don't return a
-        * divide-by-zero error code for 0 ^ -1.
+        * divide-by-zero error code for 0 ^ -1.  Raising a negative number to a
+        * non-integer power must produce the same error code, but that case is
+        * handled in power_var().
         */
        if (cmp_var(&arg1, &const_zero) == 0 &&
                cmp_var(&arg2, &const_zero) < 0)
@@ -3024,12 +3026,6 @@ numeric_power(PG_FUNCTION_ARGS)
                                (errcode(ERRCODE_INVALID_ARGUMENT_FOR_POWER_FUNCTION),
                                 errmsg("zero raised to a negative power is undefined")));
 
-       if (cmp_var(&arg1, &const_zero) < 0 &&
-               cmp_var(&arg2, &arg2_trunc) != 0)
-               ereport(ERROR,
-                               (errcode(ERRCODE_INVALID_ARGUMENT_FOR_POWER_FUNCTION),
-                                errmsg("a negative number raised to a non-integer power yields a complex result")));
-
        /*
         * Call power_var() to compute and return the result; note it handles
         * scale selection itself.
@@ -7878,12 +7874,18 @@ exp_var(const NumericVar *arg, NumericVar *result, int rscale)
         */
        val = numericvar_to_double_no_overflow(&x);
 
-       /* Guard against overflow */
+       /* Guard against overflow/underflow */
        /* If you change this limit, see also power_var()'s limit */
        if (Abs(val) >= NUMERIC_MAX_RESULT_SCALE * 3)
-               ereport(ERROR,
-                               (errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),
-                                errmsg("value overflows numeric format")));
+       {
+               if (val > 0)
+                       ereport(ERROR,
+                                       (errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),
+                                        errmsg("value overflows numeric format")));
+               zero_var(result);
+               result->dscale = rscale;
+               return;
+       }
 
        /* decimal weight = log10(e^x) = x * log10(e) */
        dweight = (int) (val * 0.434294481903252);
@@ -8228,10 +8230,13 @@ log_var(const NumericVar *base, const NumericVar *num, NumericVar *result)
 static void
 power_var(const NumericVar *base, const NumericVar *exp, NumericVar *result)
 {
+       int                     res_sign;
+       NumericVar      abs_base;
        NumericVar      ln_base;
        NumericVar      ln_num;
        int                     ln_dweight;
        int                     rscale;
+       int                     sig_digits;
        int                     local_rscale;
        double          val;
 
@@ -8271,9 +8276,40 @@ power_var(const NumericVar *base, const NumericVar *exp, NumericVar *result)
                return;
        }
 
+       init_var(&abs_base);
        init_var(&ln_base);
        init_var(&ln_num);
 
+       /*
+        * If base is negative, insist that exp be an integer.  The result is then
+        * positive if exp is even and negative if exp is odd.
+        */
+       if (base->sign == NUMERIC_NEG)
+       {
+               /*
+                * Check that exp is an integer.  This error code is defined by the
+                * SQL standard, and matches other errors in numeric_power().
+                */
+               if (exp->ndigits > 0 && exp->ndigits > exp->weight + 1)
+                       ereport(ERROR,
+                                       (errcode(ERRCODE_INVALID_ARGUMENT_FOR_POWER_FUNCTION),
+                                        errmsg("a negative number raised to a non-integer power yields a complex result")));
+
+               /* Test if exp is odd or even */
+               if (exp->ndigits > 0 && exp->ndigits == exp->weight + 1 &&
+                       (exp->digits[exp->ndigits - 1] & 1))
+                       res_sign = NUMERIC_NEG;
+               else
+                       res_sign = NUMERIC_POS;
+
+               /* Then work with abs(base) below */
+               set_var_from_var(base, &abs_base);
+               abs_base.sign = NUMERIC_POS;
+               base = &abs_base;
+       }
+       else
+               res_sign = NUMERIC_POS;
+
        /*----------
         * Decide on the scale for the ln() calculation.  For this we need an
         * estimate of the weight of the result, which we obtain by doing an
@@ -8304,11 +8340,17 @@ power_var(const NumericVar *base, const NumericVar *exp, NumericVar *result)
 
        val = numericvar_to_double_no_overflow(&ln_num);
 
-       /* initial overflow test with fuzz factor */
+       /* initial overflow/underflow test with fuzz factor */
        if (Abs(val) > NUMERIC_MAX_RESULT_SCALE * 3.01)
-               ereport(ERROR,
-                               (errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),
-                                errmsg("value overflows numeric format")));
+       {
+               if (val > 0)
+                       ereport(ERROR,
+                                       (errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),
+                                        errmsg("value overflows numeric format")));
+               zero_var(result);
+               result->dscale = NUMERIC_MAX_DISPLAY_SCALE;
+               return;
+       }
 
        val *= 0.434294481903252;       /* approximate decimal result weight */
 
@@ -8319,8 +8361,12 @@ power_var(const NumericVar *base, const NumericVar *exp, NumericVar *result)
        rscale = Max(rscale, NUMERIC_MIN_DISPLAY_SCALE);
        rscale = Min(rscale, NUMERIC_MAX_DISPLAY_SCALE);
 
+       /* significant digits required in the result */
+       sig_digits = rscale + (int) val;
+       sig_digits = Max(sig_digits, 0);
+
        /* set the scale for the real exp * ln(base) calculation */
-       local_rscale = rscale + (int) val - ln_dweight + 8;
+       local_rscale = sig_digits - ln_dweight + 8;
        local_rscale = Max(local_rscale, NUMERIC_MIN_DISPLAY_SCALE);
 
        /* and do the real calculation */
@@ -8331,8 +8377,12 @@ power_var(const NumericVar *base, const NumericVar *exp, NumericVar *result)
 
        exp_var(&ln_num, result, rscale);
 
+       if (res_sign == NUMERIC_NEG && result->ndigits > 0)
+               result->sign = NUMERIC_NEG;
+
        free_var(&ln_num);
        free_var(&ln_base);
+       free_var(&abs_base);
 }
 
 /*
index bafa02e89ee54fe1dda4471a66b889e26778c75f..579ebd79976ecbc489335dd4261c8d20e917d7e9 100644 (file)
@@ -1635,6 +1635,12 @@ select 1.000000000123 ^ (-2147483648);
  0.7678656556403084
 (1 row)
 
+select 0.9999999999 ^ 23300000000000 = 0 as rounds_to_zero;
+ rounds_to_zero 
+----------------
+ t
+(1 row)
+
 -- cases that used to error out
 select 0.12 ^ (-25);
                  ?column?                  
@@ -1648,6 +1654,43 @@ select 0.5678 ^ (-85);
  782333637740774446257.7719390061997396
 (1 row)
 
+select 0.9999999999 ^ 70000000000000 = 0 as underflows;
+ underflows 
+------------
+ t
+(1 row)
+
+-- negative base to integer powers
+select (-1.0) ^ 2147483646;
+      ?column?      
+--------------------
+ 1.0000000000000000
+(1 row)
+
+select (-1.0) ^ 2147483647;
+      ?column?       
+---------------------
+ -1.0000000000000000
+(1 row)
+
+select (-1.0) ^ 2147483648;
+      ?column?      
+--------------------
+ 1.0000000000000000
+(1 row)
+
+select (-1.0) ^ 1000000000000000;
+      ?column?      
+--------------------
+ 1.0000000000000000
+(1 row)
+
+select (-1.0) ^ 1000000000000001;
+      ?column?       
+---------------------
+ -1.0000000000000000
+(1 row)
+
 --
 -- Tests for raising to non-integer powers
 --
@@ -1766,6 +1809,18 @@ select exp(1.0::numeric(71,70));
  2.7182818284590452353602874713526624977572470936999595749669676277240766
 (1 row)
 
+select exp(-5000::numeric) = 0 as rounds_to_zero;
+ rounds_to_zero 
+----------------
+ t
+(1 row)
+
+select exp(-10000::numeric) = 0 as underflows;
+ underflows 
+------------
+ t
+(1 row)
+
 -- cases that used to generate inaccurate results
 select exp(32.999);
          exp         
index 7ab65e971008ec1ff629fe9d10454e043a278f51..2ad4f3e73875f66f5a9efe8d33dca87679a1231f 100644 (file)
@@ -899,10 +899,19 @@ select 3.789 ^ 35;
 select 1.2 ^ 345;
 select 0.12 ^ (-20);
 select 1.000000000123 ^ (-2147483648);
+select 0.9999999999 ^ 23300000000000 = 0 as rounds_to_zero;
 
 -- cases that used to error out
 select 0.12 ^ (-25);
 select 0.5678 ^ (-85);
+select 0.9999999999 ^ 70000000000000 = 0 as underflows;
+
+-- negative base to integer powers
+select (-1.0) ^ 2147483646;
+select (-1.0) ^ 2147483647;
+select (-1.0) ^ 2147483648;
+select (-1.0) ^ 1000000000000000;
+select (-1.0) ^ 1000000000000001;
 
 --
 -- Tests for raising to non-integer powers
@@ -942,6 +951,8 @@ select 1.234 ^ 5678;
 select exp(0.0);
 select exp(1.0);
 select exp(1.0::numeric(71,70));
+select exp(-5000::numeric) = 0 as rounds_to_zero;
+select exp(-10000::numeric) = 0 as underflows;
 
 -- cases that used to generate inaccurate results
 select exp(32.999);