aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTom Lane <tgl@sss.pgh.pa.us>2014-09-11 23:31:03 -0400
committerTom Lane <tgl@sss.pgh.pa.us>2014-09-11 23:31:03 -0400
commitcf5c20b063fc362792be4202990e81928dbb68a1 (patch)
treed78be4641e114f758c05285acbaae60ef5cfeece
parent7288331b9bbb717846f715cefb6712b9521d7b38 (diff)
downloadpostgresql-cf5c20b063fc362792be4202990e81928dbb68a1.tar.gz
postgresql-cf5c20b063fc362792be4202990e81928dbb68a1.zip
Fix power_var_int() for large integer exponents.
The code for raising a NUMERIC value to an integer power wasn't very careful about large powers. It got an outright wrong answer for an exponent of INT_MIN, due to failure to consider overflow of the Abs(exp) operation; which is fixable by using an unsigned rather than signed exponent value after that point. Also, even though the number of iterations of the power-computation loop is pretty limited, it's easy for the repeated squarings to result in ridiculously enormous intermediate values, which can take unreasonable amounts of time/memory to process, or even overflow the internal "weight" field and so produce a wrong answer. We can forestall misbehaviors of that sort by bailing out as soon as the weight value exceeds what will fit in int16, since then the final answer must overflow (if exp > 0) or underflow (if exp < 0) the packed numeric format. Per off-list report from Pavel Stehule. Back-patch to all supported branches.
-rw-r--r--src/backend/utils/adt/numeric.c30
-rw-r--r--src/test/regress/expected/numeric.out19
-rw-r--r--src/test/regress/sql/numeric.sql9
3 files changed, 54 insertions, 4 deletions
diff --git a/src/backend/utils/adt/numeric.c b/src/backend/utils/adt/numeric.c
index 5091306b0e9..2ebfcb6e6f7 100644
--- a/src/backend/utils/adt/numeric.c
+++ b/src/backend/utils/adt/numeric.c
@@ -5649,10 +5649,12 @@ power_var(NumericVar *base, NumericVar *exp, NumericVar *result)
static void
power_var_int(NumericVar *base, int exp, NumericVar *result, int rscale)
{
+ unsigned int mask;
bool neg;
NumericVar base_prod;
int local_rscale;
+ /* Handle some common special cases, as well as corner cases */
switch (exp)
{
case 0:
@@ -5686,23 +5688,43 @@ power_var_int(NumericVar *base, int exp, NumericVar *result, int rscale)
* pattern of exp. We do the multiplications with some extra precision.
*/
neg = (exp < 0);
- exp = Abs(exp);
+ mask = Abs(exp);
local_rscale = rscale + MUL_GUARD_DIGITS * 2;
init_var(&base_prod);
set_var_from_var(base, &base_prod);
- if (exp & 1)
+ if (mask & 1)
set_var_from_var(base, result);
else
set_var_from_var(&const_one, result);
- while ((exp >>= 1) > 0)
+ while ((mask >>= 1) > 0)
{
mul_var(&base_prod, &base_prod, &base_prod, local_rscale);
- if (exp & 1)
+ if (mask & 1)
mul_var(&base_prod, result, result, local_rscale);
+
+ /*
+ * When abs(base) > 1, the number of digits to the left of the decimal
+ * point in base_prod doubles at each iteration, so if exp is large we
+ * could easily spend large amounts of time and memory space doing the
+ * multiplications. But once the weight exceeds what will fit in
+ * int16, the final result is guaranteed to overflow (or underflow, if
+ * exp < 0), so we can give up before wasting too many cycles.
+ */
+ if (base_prod.weight > SHRT_MAX || result->weight > SHRT_MAX)
+ {
+ /* overflow, unless neg, in which case result should be 0 */
+ if (!neg)
+ ereport(ERROR,
+ (errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),
+ errmsg("value overflows numeric format")));
+ zero_var(result);
+ neg = false;
+ break;
+ }
}
free_var(&base_prod);
diff --git a/src/test/regress/expected/numeric.out b/src/test/regress/expected/numeric.out
index 71c520e5e7a..a8da4cdaa2d 100644
--- a/src/test/regress/expected/numeric.out
+++ b/src/test/regress/expected/numeric.out
@@ -1390,3 +1390,22 @@ select div(12345678901234567890, 123) * 123 + 12345678901234567890 % 123;
12345678901234567890
(1 row)
+--
+-- Test code path for raising to integer powers
+--
+select 10.0 ^ -2147483648 as rounds_to_zero;
+ rounds_to_zero
+--------------------
+ 0.0000000000000000
+(1 row)
+
+select 10.0 ^ -2147483647 as rounds_to_zero;
+ rounds_to_zero
+--------------------
+ 0.0000000000000000
+(1 row)
+
+select 10.0 ^ 2147483647 as overflows;
+ERROR: value overflows numeric format
+select 117743296169.0 ^ 1000000000 as overflows;
+ERROR: value overflows numeric format
diff --git a/src/test/regress/sql/numeric.sql b/src/test/regress/sql/numeric.sql
index b2dc46fae28..5c08717e7a9 100644
--- a/src/test/regress/sql/numeric.sql
+++ b/src/test/regress/sql/numeric.sql
@@ -828,3 +828,12 @@ select 12345678901234567890 % 123;
select 12345678901234567890 / 123;
select div(12345678901234567890, 123);
select div(12345678901234567890, 123) * 123 + 12345678901234567890 % 123;
+
+--
+-- Test code path for raising to integer powers
+--
+
+select 10.0 ^ -2147483648 as rounds_to_zero;
+select 10.0 ^ -2147483647 as rounds_to_zero;
+select 10.0 ^ 2147483647 as overflows;
+select 117743296169.0 ^ 1000000000 as overflows;