Skip to content

Commit 053ec4e

Browse files
committed
Fix corner-case errors and loss of precision in numeric_power().
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
1 parent 171bf1c commit 053ec4e

File tree

3 files changed

+132
-16
lines changed

3 files changed

+132
-16
lines changed

src/backend/utils/adt/numeric.c

Lines changed: 66 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3238,20 +3238,16 @@ numeric_power(PG_FUNCTION_ARGS)
32383238
/*
32393239
* The SQL spec requires that we emit a particular SQLSTATE error code for
32403240
* certain error conditions. Specifically, we don't return a
3241-
* divide-by-zero error code for 0 ^ -1.
3241+
* divide-by-zero error code for 0 ^ -1. Raising a negative number to a
3242+
* non-integer power must produce the same error code, but that case is
3243+
* handled in power_var().
32423244
*/
32433245
if (cmp_var(&arg1, &const_zero) == 0 &&
32443246
cmp_var(&arg2, &const_zero) < 0)
32453247
ereport(ERROR,
32463248
(errcode(ERRCODE_INVALID_ARGUMENT_FOR_POWER_FUNCTION),
32473249
errmsg("zero raised to a negative power is undefined")));
32483250

3249-
if (cmp_var(&arg1, &const_zero) < 0 &&
3250-
cmp_var(&arg2, &arg2_trunc) != 0)
3251-
ereport(ERROR,
3252-
(errcode(ERRCODE_INVALID_ARGUMENT_FOR_POWER_FUNCTION),
3253-
errmsg("a negative number raised to a non-integer power yields a complex result")));
3254-
32553251
/*
32563252
* Call power_var() to compute and return the result; note it handles
32573253
* scale selection itself.
@@ -8802,12 +8798,18 @@ exp_var(const NumericVar *arg, NumericVar *result, int rscale)
88028798
*/
88038799
val = numericvar_to_double_no_overflow(&x);
88048800

8805-
/* Guard against overflow */
8801+
/* Guard against overflow/underflow */
88068802
/* If you change this limit, see also power_var()'s limit */
88078803
if (Abs(val) >= NUMERIC_MAX_RESULT_SCALE * 3)
8808-
ereport(ERROR,
8809-
(errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),
8810-
errmsg("value overflows numeric format")));
8804+
{
8805+
if (val > 0)
8806+
ereport(ERROR,
8807+
(errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),
8808+
errmsg("value overflows numeric format")));
8809+
zero_var(result);
8810+
result->dscale = rscale;
8811+
return;
8812+
}
88118813

88128814
/* decimal weight = log10(e^x) = x * log10(e) */
88138815
dweight = (int) (val * 0.434294481903252);
@@ -9165,10 +9167,13 @@ log_var(const NumericVar *base, const NumericVar *num, NumericVar *result)
91659167
static void
91669168
power_var(const NumericVar *base, const NumericVar *exp, NumericVar *result)
91679169
{
9170+
int res_sign;
9171+
NumericVar abs_base;
91689172
NumericVar ln_base;
91699173
NumericVar ln_num;
91709174
int ln_dweight;
91719175
int rscale;
9176+
int sig_digits;
91729177
int local_rscale;
91739178
double val;
91749179

@@ -9208,9 +9213,40 @@ power_var(const NumericVar *base, const NumericVar *exp, NumericVar *result)
92089213
return;
92099214
}
92109215

9216+
init_var(&abs_base);
92119217
init_var(&ln_base);
92129218
init_var(&ln_num);
92139219

9220+
/*
9221+
* If base is negative, insist that exp be an integer. The result is then
9222+
* positive if exp is even and negative if exp is odd.
9223+
*/
9224+
if (base->sign == NUMERIC_NEG)
9225+
{
9226+
/*
9227+
* Check that exp is an integer. This error code is defined by the
9228+
* SQL standard, and matches other errors in numeric_power().
9229+
*/
9230+
if (exp->ndigits > 0 && exp->ndigits > exp->weight + 1)
9231+
ereport(ERROR,
9232+
(errcode(ERRCODE_INVALID_ARGUMENT_FOR_POWER_FUNCTION),
9233+
errmsg("a negative number raised to a non-integer power yields a complex result")));
9234+
9235+
/* Test if exp is odd or even */
9236+
if (exp->ndigits > 0 && exp->ndigits == exp->weight + 1 &&
9237+
(exp->digits[exp->ndigits - 1] & 1))
9238+
res_sign = NUMERIC_NEG;
9239+
else
9240+
res_sign = NUMERIC_POS;
9241+
9242+
/* Then work with abs(base) below */
9243+
set_var_from_var(base, &abs_base);
9244+
abs_base.sign = NUMERIC_POS;
9245+
base = &abs_base;
9246+
}
9247+
else
9248+
res_sign = NUMERIC_POS;
9249+
92149250
/*----------
92159251
* Decide on the scale for the ln() calculation. For this we need an
92169252
* estimate of the weight of the result, which we obtain by doing an
@@ -9241,11 +9277,17 @@ power_var(const NumericVar *base, const NumericVar *exp, NumericVar *result)
92419277

92429278
val = numericvar_to_double_no_overflow(&ln_num);
92439279

9244-
/* initial overflow test with fuzz factor */
9280+
/* initial overflow/underflow test with fuzz factor */
92459281
if (Abs(val) > NUMERIC_MAX_RESULT_SCALE * 3.01)
9246-
ereport(ERROR,
9247-
(errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),
9248-
errmsg("value overflows numeric format")));
9282+
{
9283+
if (val > 0)
9284+
ereport(ERROR,
9285+
(errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),
9286+
errmsg("value overflows numeric format")));
9287+
zero_var(result);
9288+
result->dscale = NUMERIC_MAX_DISPLAY_SCALE;
9289+
return;
9290+
}
92499291

92509292
val *= 0.434294481903252; /* approximate decimal result weight */
92519293

@@ -9256,8 +9298,12 @@ power_var(const NumericVar *base, const NumericVar *exp, NumericVar *result)
92569298
rscale = Max(rscale, NUMERIC_MIN_DISPLAY_SCALE);
92579299
rscale = Min(rscale, NUMERIC_MAX_DISPLAY_SCALE);
92589300

9301+
/* significant digits required in the result */
9302+
sig_digits = rscale + (int) val;
9303+
sig_digits = Max(sig_digits, 0);
9304+
92599305
/* set the scale for the real exp * ln(base) calculation */
9260-
local_rscale = rscale + (int) val - ln_dweight + 8;
9306+
local_rscale = sig_digits - ln_dweight + 8;
92619307
local_rscale = Max(local_rscale, NUMERIC_MIN_DISPLAY_SCALE);
92629308

92639309
/* and do the real calculation */
@@ -9268,8 +9314,12 @@ power_var(const NumericVar *base, const NumericVar *exp, NumericVar *result)
92689314

92699315
exp_var(&ln_num, result, rscale);
92709316

9317+
if (res_sign == NUMERIC_NEG && result->ndigits > 0)
9318+
result->sign = NUMERIC_NEG;
9319+
92719320
free_var(&ln_num);
92729321
free_var(&ln_base);
9322+
free_var(&abs_base);
92739323
}
92749324

92759325
/*

src/test/regress/expected/numeric.out

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1686,6 +1686,12 @@ select 1.000000000123 ^ (-2147483648);
16861686
0.7678656556403084
16871687
(1 row)
16881688

1689+
select 0.9999999999 ^ 23300000000000 = 0 as rounds_to_zero;
1690+
rounds_to_zero
1691+
----------------
1692+
t
1693+
(1 row)
1694+
16891695
-- cases that used to error out
16901696
select 0.12 ^ (-25);
16911697
?column?
@@ -1699,6 +1705,43 @@ select 0.5678 ^ (-85);
16991705
782333637740774446257.7719390061997396
17001706
(1 row)
17011707

1708+
select 0.9999999999 ^ 70000000000000 = 0 as underflows;
1709+
underflows
1710+
------------
1711+
t
1712+
(1 row)
1713+
1714+
-- negative base to integer powers
1715+
select (-1.0) ^ 2147483646;
1716+
?column?
1717+
--------------------
1718+
1.0000000000000000
1719+
(1 row)
1720+
1721+
select (-1.0) ^ 2147483647;
1722+
?column?
1723+
---------------------
1724+
-1.0000000000000000
1725+
(1 row)
1726+
1727+
select (-1.0) ^ 2147483648;
1728+
?column?
1729+
--------------------
1730+
1.0000000000000000
1731+
(1 row)
1732+
1733+
select (-1.0) ^ 1000000000000000;
1734+
?column?
1735+
--------------------
1736+
1.0000000000000000
1737+
(1 row)
1738+
1739+
select (-1.0) ^ 1000000000000001;
1740+
?column?
1741+
---------------------
1742+
-1.0000000000000000
1743+
(1 row)
1744+
17021745
--
17031746
-- Tests for raising to non-integer powers
17041747
--
@@ -1817,6 +1860,18 @@ select exp(1.0::numeric(71,70));
18171860
2.7182818284590452353602874713526624977572470936999595749669676277240766
18181861
(1 row)
18191862

1863+
select exp(-5000::numeric) = 0 as rounds_to_zero;
1864+
rounds_to_zero
1865+
----------------
1866+
t
1867+
(1 row)
1868+
1869+
select exp(-10000::numeric) = 0 as underflows;
1870+
underflows
1871+
------------
1872+
t
1873+
(1 row)
1874+
18201875
-- cases that used to generate inaccurate results
18211876
select exp(32.999);
18221877
exp

src/test/regress/sql/numeric.sql

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -912,10 +912,19 @@ select 3.789 ^ 35;
912912
select 1.2 ^ 345;
913913
select 0.12 ^ (-20);
914914
select 1.000000000123 ^ (-2147483648);
915+
select 0.9999999999 ^ 23300000000000 = 0 as rounds_to_zero;
915916

916917
-- cases that used to error out
917918
select 0.12 ^ (-25);
918919
select 0.5678 ^ (-85);
920+
select 0.9999999999 ^ 70000000000000 = 0 as underflows;
921+
922+
-- negative base to integer powers
923+
select (-1.0) ^ 2147483646;
924+
select (-1.0) ^ 2147483647;
925+
select (-1.0) ^ 2147483648;
926+
select (-1.0) ^ 1000000000000000;
927+
select (-1.0) ^ 1000000000000001;
919928

920929
--
921930
-- Tests for raising to non-integer powers
@@ -955,6 +964,8 @@ select 1.234 ^ 5678;
955964
select exp(0.0);
956965
select exp(1.0);
957966
select exp(1.0::numeric(71,70));
967+
select exp(-5000::numeric) = 0 as rounds_to_zero;
968+
select exp(-10000::numeric) = 0 as underflows;
958969

959970
-- cases that used to generate inaccurate results
960971
select exp(32.999);

0 commit comments

Comments
 (0)