Location via proxy:   [ UP ]  
[Report a bug]   [Manage cookies]                
Skip to content

Commit e473e1f

Browse files
committed
Fix float-to-integer coercions to handle edge cases correctly.
ftoi4 and its sibling coercion functions did their overflow checks in a way that looked superficially plausible, but actually depended on an assumption that the MIN and MAX comparison constants can be represented exactly in the float4 or float8 domain. That fails in ftoi4, ftoi8, and dtoi8, resulting in a possibility that values near the MAX limit will be wrongly converted (to negative values) when they need to be rejected. Also, because we compared before rounding off the fractional part, the other three functions threw errors for values that really ought to get rounded to the min or max integer value. Fix by doing rint() first (requiring an assumption that it handles NaN and Inf correctly; but dtoi8 and ftoi8 were assuming that already), and by comparing to values that should coerce to float exactly, namely INTxx_MIN and -INTxx_MIN. Also remove some random cosmetic discrepancies between these six functions. This back-patches commits cbdb8b4 and 452b637. In the 9.4 branch, also back-patch the portion of 62e2a8d that added PG_INTnn_MIN and related constants to c.h, so that these functions can rely on them. Per bug #15519 from Victor Petrovykh. Patch by me; thanks to Andrew Gierth for analysis and discussion. Discussion: https://postgr.es/m/15519-4fc785b483201ff1@postgresql.org
1 parent 2e497ed commit e473e1f

File tree

7 files changed

+277
-29
lines changed

7 files changed

+277
-29
lines changed

src/backend/utils/adt/float.c

Lines changed: 68 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1354,16 +1354,28 @@ Datum
13541354
dtoi4(PG_FUNCTION_ARGS)
13551355
{
13561356
float8 num = PG_GETARG_FLOAT8(0);
1357-
int32 result;
13581357

1359-
/* 'Inf' is handled by INT_MAX */
1360-
if (num < INT_MIN || num > INT_MAX || isnan(num))
1358+
/*
1359+
* Get rid of any fractional part in the input. This is so we don't fail
1360+
* on just-out-of-range values that would round into range. Note
1361+
* assumption that rint() will pass through a NaN or Inf unchanged.
1362+
*/
1363+
num = rint(num);
1364+
1365+
/*
1366+
* Range check. We must be careful here that the boundary values are
1367+
* expressed exactly in the float domain. We expect PG_INT32_MIN to be an
1368+
* exact power of 2, so it will be represented exactly; but PG_INT32_MAX
1369+
* isn't, and might get rounded off, so avoid using it.
1370+
*/
1371+
if (unlikely(num < (float8) PG_INT32_MIN ||
1372+
num >= -((float8) PG_INT32_MIN) ||
1373+
isnan(num)))
13611374
ereport(ERROR,
13621375
(errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),
13631376
errmsg("integer out of range")));
13641377

1365-
result = (int32) rint(num);
1366-
PG_RETURN_INT32(result);
1378+
PG_RETURN_INT32((int32) num);
13671379
}
13681380

13691381

@@ -1375,12 +1387,27 @@ dtoi2(PG_FUNCTION_ARGS)
13751387
{
13761388
float8 num = PG_GETARG_FLOAT8(0);
13771389

1378-
if (num < SHRT_MIN || num > SHRT_MAX || isnan(num))
1390+
/*
1391+
* Get rid of any fractional part in the input. This is so we don't fail
1392+
* on just-out-of-range values that would round into range. Note
1393+
* assumption that rint() will pass through a NaN or Inf unchanged.
1394+
*/
1395+
num = rint(num);
1396+
1397+
/*
1398+
* Range check. We must be careful here that the boundary values are
1399+
* expressed exactly in the float domain. We expect PG_INT16_MIN to be an
1400+
* exact power of 2, so it will be represented exactly; but PG_INT16_MAX
1401+
* isn't, and might get rounded off, so avoid using it.
1402+
*/
1403+
if (unlikely(num < (float8) PG_INT16_MIN ||
1404+
num >= -((float8) PG_INT16_MIN) ||
1405+
isnan(num)))
13791406
ereport(ERROR,
13801407
(errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),
13811408
errmsg("smallint out of range")));
13821409

1383-
PG_RETURN_INT16((int16) rint(num));
1410+
PG_RETURN_INT16((int16) num);
13841411
}
13851412

13861413

@@ -1416,12 +1443,27 @@ ftoi4(PG_FUNCTION_ARGS)
14161443
{
14171444
float4 num = PG_GETARG_FLOAT4(0);
14181445

1419-
if (num < INT_MIN || num > INT_MAX || isnan(num))
1446+
/*
1447+
* Get rid of any fractional part in the input. This is so we don't fail
1448+
* on just-out-of-range values that would round into range. Note
1449+
* assumption that rint() will pass through a NaN or Inf unchanged.
1450+
*/
1451+
num = rint(num);
1452+
1453+
/*
1454+
* Range check. We must be careful here that the boundary values are
1455+
* expressed exactly in the float domain. We expect PG_INT32_MIN to be an
1456+
* exact power of 2, so it will be represented exactly; but PG_INT32_MAX
1457+
* isn't, and might get rounded off, so avoid using it.
1458+
*/
1459+
if (unlikely(num < (float4) PG_INT32_MIN ||
1460+
num >= -((float4) PG_INT32_MIN) ||
1461+
isnan(num)))
14201462
ereport(ERROR,
14211463
(errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),
14221464
errmsg("integer out of range")));
14231465

1424-
PG_RETURN_INT32((int32) rint(num));
1466+
PG_RETURN_INT32((int32) num);
14251467
}
14261468

14271469

@@ -1433,12 +1475,27 @@ ftoi2(PG_FUNCTION_ARGS)
14331475
{
14341476
float4 num = PG_GETARG_FLOAT4(0);
14351477

1436-
if (num < SHRT_MIN || num > SHRT_MAX || isnan(num))
1478+
/*
1479+
* Get rid of any fractional part in the input. This is so we don't fail
1480+
* on just-out-of-range values that would round into range. Note
1481+
* assumption that rint() will pass through a NaN or Inf unchanged.
1482+
*/
1483+
num = rint(num);
1484+
1485+
/*
1486+
* Range check. We must be careful here that the boundary values are
1487+
* expressed exactly in the float domain. We expect PG_INT16_MIN to be an
1488+
* exact power of 2, so it will be represented exactly; but PG_INT16_MAX
1489+
* isn't, and might get rounded off, so avoid using it.
1490+
*/
1491+
if (unlikely(num < (float4) PG_INT16_MIN ||
1492+
num >= -((float4) PG_INT16_MIN) ||
1493+
isnan(num)))
14371494
ereport(ERROR,
14381495
(errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),
14391496
errmsg("smallint out of range")));
14401497

1441-
PG_RETURN_INT16((int16) rint(num));
1498+
PG_RETURN_INT16((int16) num);
14421499
}
14431500

14441501

src/backend/utils/adt/int8.c

Lines changed: 34 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1204,22 +1204,29 @@ i8tod(PG_FUNCTION_ARGS)
12041204
Datum
12051205
dtoi8(PG_FUNCTION_ARGS)
12061206
{
1207-
float8 arg = PG_GETARG_FLOAT8(0);
1208-
int64 result;
1207+
float8 num = PG_GETARG_FLOAT8(0);
12091208

1210-
/* Round arg to nearest integer (but it's still in float form) */
1211-
arg = rint(arg);
1209+
/*
1210+
* Get rid of any fractional part in the input. This is so we don't fail
1211+
* on just-out-of-range values that would round into range. Note
1212+
* assumption that rint() will pass through a NaN or Inf unchanged.
1213+
*/
1214+
num = rint(num);
12121215

1213-
if (unlikely(arg < (double) PG_INT64_MIN) ||
1214-
unlikely(arg > (double) PG_INT64_MAX) ||
1215-
unlikely(isnan(arg)))
1216+
/*
1217+
* Range check. We must be careful here that the boundary values are
1218+
* expressed exactly in the float domain. We expect PG_INT64_MIN to be an
1219+
* exact power of 2, so it will be represented exactly; but PG_INT64_MAX
1220+
* isn't, and might get rounded off, so avoid using it.
1221+
*/
1222+
if (unlikely(num < (float8) PG_INT64_MIN ||
1223+
num >= -((float8) PG_INT64_MIN) ||
1224+
isnan(num)))
12161225
ereport(ERROR,
12171226
(errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),
12181227
errmsg("bigint out of range")));
12191228

1220-
result = (int64) arg;
1221-
1222-
PG_RETURN_INT64(result);
1229+
PG_RETURN_INT64((int64) num);
12231230
}
12241231

12251232
Datum
@@ -1239,20 +1246,29 @@ i8tof(PG_FUNCTION_ARGS)
12391246
Datum
12401247
ftoi8(PG_FUNCTION_ARGS)
12411248
{
1242-
float4 arg = PG_GETARG_FLOAT4(0);
1243-
float8 darg;
1249+
float4 num = PG_GETARG_FLOAT4(0);
12441250

1245-
/* Round arg to nearest integer (but it's still in float form) */
1246-
darg = rint(arg);
1251+
/*
1252+
* Get rid of any fractional part in the input. This is so we don't fail
1253+
* on just-out-of-range values that would round into range. Note
1254+
* assumption that rint() will pass through a NaN or Inf unchanged.
1255+
*/
1256+
num = rint(num);
12471257

1248-
if (unlikely(arg < (float4) PG_INT64_MIN) ||
1249-
unlikely(arg > (float4) PG_INT64_MAX) ||
1250-
unlikely(isnan(arg)))
1258+
/*
1259+
* Range check. We must be careful here that the boundary values are
1260+
* expressed exactly in the float domain. We expect PG_INT64_MIN to be an
1261+
* exact power of 2, so it will be represented exactly; but PG_INT64_MAX
1262+
* isn't, and might get rounded off, so avoid using it.
1263+
*/
1264+
if (unlikely(num < (float4) PG_INT64_MIN ||
1265+
num >= -((float4) PG_INT64_MIN) ||
1266+
isnan(num)))
12511267
ereport(ERROR,
12521268
(errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),
12531269
errmsg("bigint out of range")));
12541270

1255-
PG_RETURN_INT64((int64) darg);
1271+
PG_RETURN_INT64((int64) num);
12561272
}
12571273

12581274
Datum

src/test/regress/expected/float4.out

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,3 +257,52 @@ SELECT '' AS five, * FROM FLOAT4_TBL;
257257
| -1.23457e-20
258258
(5 rows)
259259

260+
-- test edge-case coercions to integer
261+
SELECT '32767.4'::float4::int2;
262+
int2
263+
-------
264+
32767
265+
(1 row)
266+
267+
SELECT '32767.6'::float4::int2;
268+
ERROR: smallint out of range
269+
SELECT '-32768.4'::float4::int2;
270+
int2
271+
--------
272+
-32768
273+
(1 row)
274+
275+
SELECT '-32768.6'::float4::int2;
276+
ERROR: smallint out of range
277+
SELECT '2147483520'::float4::int4;
278+
int4
279+
------------
280+
2147483520
281+
(1 row)
282+
283+
SELECT '2147483647'::float4::int4;
284+
ERROR: integer out of range
285+
SELECT '-2147483648.5'::float4::int4;
286+
int4
287+
-------------
288+
-2147483648
289+
(1 row)
290+
291+
SELECT '-2147483900'::float4::int4;
292+
ERROR: integer out of range
293+
SELECT '9223369837831520256'::float4::int8;
294+
int8
295+
---------------------
296+
9223369837831520256
297+
(1 row)
298+
299+
SELECT '9223372036854775807'::float4::int8;
300+
ERROR: bigint out of range
301+
SELECT '-9223372036854775808.5'::float4::int8;
302+
int8
303+
----------------------
304+
-9223372036854775808
305+
(1 row)
306+
307+
SELECT '-9223380000000000000'::float4::int8;
308+
ERROR: bigint out of range

src/test/regress/expected/float8-small-is-zero.out

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -478,6 +478,55 @@ SELECT '' AS five, * FROM FLOAT8_TBL;
478478
| -1.2345678901234e-200
479479
(5 rows)
480480

481+
-- test edge-case coercions to integer
482+
SELECT '32767.4'::float8::int2;
483+
int2
484+
-------
485+
32767
486+
(1 row)
487+
488+
SELECT '32767.6'::float8::int2;
489+
ERROR: smallint out of range
490+
SELECT '-32768.4'::float8::int2;
491+
int2
492+
--------
493+
-32768
494+
(1 row)
495+
496+
SELECT '-32768.6'::float8::int2;
497+
ERROR: smallint out of range
498+
SELECT '2147483647.4'::float8::int4;
499+
int4
500+
------------
501+
2147483647
502+
(1 row)
503+
504+
SELECT '2147483647.6'::float8::int4;
505+
ERROR: integer out of range
506+
SELECT '-2147483648.4'::float8::int4;
507+
int4
508+
-------------
509+
-2147483648
510+
(1 row)
511+
512+
SELECT '-2147483648.6'::float8::int4;
513+
ERROR: integer out of range
514+
SELECT '9223372036854773760'::float8::int8;
515+
int8
516+
---------------------
517+
9223372036854773760
518+
(1 row)
519+
520+
SELECT '9223372036854775807'::float8::int8;
521+
ERROR: bigint out of range
522+
SELECT '-9223372036854775808.5'::float8::int8;
523+
int8
524+
----------------------
525+
-9223372036854775808
526+
(1 row)
527+
528+
SELECT '-9223372036854780000'::float8::int8;
529+
ERROR: bigint out of range
481530
-- test exact cases for trigonometric functions in degrees
482531
SET extra_float_digits = 3;
483532
SELECT x,

src/test/regress/expected/float8.out

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -480,6 +480,55 @@ SELECT '' AS five, * FROM FLOAT8_TBL;
480480
| -1.2345678901234e-200
481481
(5 rows)
482482

483+
-- test edge-case coercions to integer
484+
SELECT '32767.4'::float8::int2;
485+
int2
486+
-------
487+
32767
488+
(1 row)
489+
490+
SELECT '32767.6'::float8::int2;
491+
ERROR: smallint out of range
492+
SELECT '-32768.4'::float8::int2;
493+
int2
494+
--------
495+
-32768
496+
(1 row)
497+
498+
SELECT '-32768.6'::float8::int2;
499+
ERROR: smallint out of range
500+
SELECT '2147483647.4'::float8::int4;
501+
int4
502+
------------
503+
2147483647
504+
(1 row)
505+
506+
SELECT '2147483647.6'::float8::int4;
507+
ERROR: integer out of range
508+
SELECT '-2147483648.4'::float8::int4;
509+
int4
510+
-------------
511+
-2147483648
512+
(1 row)
513+
514+
SELECT '-2147483648.6'::float8::int4;
515+
ERROR: integer out of range
516+
SELECT '9223372036854773760'::float8::int8;
517+
int8
518+
---------------------
519+
9223372036854773760
520+
(1 row)
521+
522+
SELECT '9223372036854775807'::float8::int8;
523+
ERROR: bigint out of range
524+
SELECT '-9223372036854775808.5'::float8::int8;
525+
int8
526+
----------------------
527+
-9223372036854775808
528+
(1 row)
529+
530+
SELECT '-9223372036854780000'::float8::int8;
531+
ERROR: bigint out of range
483532
-- test exact cases for trigonometric functions in degrees
484533
SET extra_float_digits = 3;
485534
SELECT x,

src/test/regress/sql/float4.sql

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,3 +81,17 @@ UPDATE FLOAT4_TBL
8181
WHERE FLOAT4_TBL.f1 > '0.0';
8282

8383
SELECT '' AS five, * FROM FLOAT4_TBL;
84+
85+
-- test edge-case coercions to integer
86+
SELECT '32767.4'::float4::int2;
87+
SELECT '32767.6'::float4::int2;
88+
SELECT '-32768.4'::float4::int2;
89+
SELECT '-32768.6'::float4::int2;
90+
SELECT '2147483520'::float4::int4;
91+
SELECT '2147483647'::float4::int4;
92+
SELECT '-2147483648.5'::float4::int4;
93+
SELECT '-2147483900'::float4::int4;
94+
SELECT '9223369837831520256'::float4::int8;
95+
SELECT '9223372036854775807'::float4::int8;
96+
SELECT '-9223372036854775808.5'::float4::int8;
97+
SELECT '-9223380000000000000'::float4::int8;

0 commit comments

Comments
 (0)