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

Commit 93eec12

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 5f11a50 commit 93eec12

File tree

7 files changed

+273
-34
lines changed

7 files changed

+273
-34
lines changed

src/backend/utils/adt/float.c

Lines changed: 68 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1219,16 +1219,28 @@ Datum
12191219
dtoi4(PG_FUNCTION_ARGS)
12201220
{
12211221
float8 num = PG_GETARG_FLOAT8(0);
1222-
int32 result;
12231222

1224-
/* 'Inf' is handled by INT_MAX */
1225-
if (num < INT_MIN || num > INT_MAX || isnan(num))
1223+
/*
1224+
* Get rid of any fractional part in the input. This is so we don't fail
1225+
* on just-out-of-range values that would round into range. Note
1226+
* assumption that rint() will pass through a NaN or Inf unchanged.
1227+
*/
1228+
num = rint(num);
1229+
1230+
/*
1231+
* Range check. We must be careful here that the boundary values are
1232+
* expressed exactly in the float domain. We expect PG_INT32_MIN to be an
1233+
* exact power of 2, so it will be represented exactly; but PG_INT32_MAX
1234+
* isn't, and might get rounded off, so avoid using it.
1235+
*/
1236+
if (num < (float8) PG_INT32_MIN ||
1237+
num >= -((float8) PG_INT32_MIN) ||
1238+
isnan(num))
12261239
ereport(ERROR,
12271240
(errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),
12281241
errmsg("integer out of range")));
12291242

1230-
result = (int32) rint(num);
1231-
PG_RETURN_INT32(result);
1243+
PG_RETURN_INT32((int32) num);
12321244
}
12331245

12341246

@@ -1240,12 +1252,27 @@ dtoi2(PG_FUNCTION_ARGS)
12401252
{
12411253
float8 num = PG_GETARG_FLOAT8(0);
12421254

1243-
if (num < SHRT_MIN || num > SHRT_MAX || isnan(num))
1255+
/*
1256+
* Get rid of any fractional part in the input. This is so we don't fail
1257+
* on just-out-of-range values that would round into range. Note
1258+
* assumption that rint() will pass through a NaN or Inf unchanged.
1259+
*/
1260+
num = rint(num);
1261+
1262+
/*
1263+
* Range check. We must be careful here that the boundary values are
1264+
* expressed exactly in the float domain. We expect PG_INT16_MIN to be an
1265+
* exact power of 2, so it will be represented exactly; but PG_INT16_MAX
1266+
* isn't, and might get rounded off, so avoid using it.
1267+
*/
1268+
if (num < (float8) PG_INT16_MIN ||
1269+
num >= -((float8) PG_INT16_MIN) ||
1270+
isnan(num))
12441271
ereport(ERROR,
12451272
(errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),
12461273
errmsg("smallint out of range")));
12471274

1248-
PG_RETURN_INT16((int16) rint(num));
1275+
PG_RETURN_INT16((int16) num);
12491276
}
12501277

12511278

@@ -1281,12 +1308,27 @@ ftoi4(PG_FUNCTION_ARGS)
12811308
{
12821309
float4 num = PG_GETARG_FLOAT4(0);
12831310

1284-
if (num < INT_MIN || num > INT_MAX || isnan(num))
1311+
/*
1312+
* Get rid of any fractional part in the input. This is so we don't fail
1313+
* on just-out-of-range values that would round into range. Note
1314+
* assumption that rint() will pass through a NaN or Inf unchanged.
1315+
*/
1316+
num = rint(num);
1317+
1318+
/*
1319+
* Range check. We must be careful here that the boundary values are
1320+
* expressed exactly in the float domain. We expect PG_INT32_MIN to be an
1321+
* exact power of 2, so it will be represented exactly; but PG_INT32_MAX
1322+
* isn't, and might get rounded off, so avoid using it.
1323+
*/
1324+
if (num < (float4) PG_INT32_MIN ||
1325+
num >= -((float4) PG_INT32_MIN) ||
1326+
isnan(num))
12851327
ereport(ERROR,
12861328
(errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),
12871329
errmsg("integer out of range")));
12881330

1289-
PG_RETURN_INT32((int32) rint(num));
1331+
PG_RETURN_INT32((int32) num);
12901332
}
12911333

12921334

@@ -1298,12 +1340,27 @@ ftoi2(PG_FUNCTION_ARGS)
12981340
{
12991341
float4 num = PG_GETARG_FLOAT4(0);
13001342

1301-
if (num < SHRT_MIN || num > SHRT_MAX || isnan(num))
1343+
/*
1344+
* Get rid of any fractional part in the input. This is so we don't fail
1345+
* on just-out-of-range values that would round into range. Note
1346+
* assumption that rint() will pass through a NaN or Inf unchanged.
1347+
*/
1348+
num = rint(num);
1349+
1350+
/*
1351+
* Range check. We must be careful here that the boundary values are
1352+
* expressed exactly in the float domain. We expect PG_INT16_MIN to be an
1353+
* exact power of 2, so it will be represented exactly; but PG_INT16_MAX
1354+
* isn't, and might get rounded off, so avoid using it.
1355+
*/
1356+
if (num < (float4) PG_INT16_MIN ||
1357+
num >= -((float4) PG_INT16_MIN) ||
1358+
isnan(num))
13021359
ereport(ERROR,
13031360
(errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),
13041361
errmsg("smallint out of range")));
13051362

1306-
PG_RETURN_INT16((int16) rint(num));
1363+
PG_RETURN_INT16((int16) num);
13071364
}
13081365

13091366

src/backend/utils/adt/int8.c

Lines changed: 30 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1342,25 +1342,29 @@ i8tod(PG_FUNCTION_ARGS)
13421342
Datum
13431343
dtoi8(PG_FUNCTION_ARGS)
13441344
{
1345-
float8 arg = PG_GETARG_FLOAT8(0);
1346-
int64 result;
1347-
1348-
/* Round arg to nearest integer (but it's still in float form) */
1349-
arg = rint(arg);
1345+
float8 num = PG_GETARG_FLOAT8(0);
13501346

13511347
/*
1352-
* Does it fit in an int64? Avoid assuming that we have handy constants
1353-
* defined for the range boundaries, instead test for overflow by
1354-
* reverse-conversion.
1348+
* Get rid of any fractional part in the input. This is so we don't fail
1349+
* on just-out-of-range values that would round into range. Note
1350+
* assumption that rint() will pass through a NaN or Inf unchanged.
13551351
*/
1356-
result = (int64) arg;
1352+
num = rint(num);
13571353

1358-
if ((float8) result != arg)
1354+
/*
1355+
* Range check. We must be careful here that the boundary values are
1356+
* expressed exactly in the float domain. We expect PG_INT64_MIN to be an
1357+
* exact power of 2, so it will be represented exactly; but PG_INT64_MAX
1358+
* isn't, and might get rounded off, so avoid using it.
1359+
*/
1360+
if (num < (float8) PG_INT64_MIN ||
1361+
num >= -((float8) PG_INT64_MIN) ||
1362+
isnan(num))
13591363
ereport(ERROR,
13601364
(errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),
13611365
errmsg("bigint out of range")));
13621366

1363-
PG_RETURN_INT64(result);
1367+
PG_RETURN_INT64((int64) num);
13641368
}
13651369

13661370
Datum
@@ -1380,26 +1384,29 @@ i8tof(PG_FUNCTION_ARGS)
13801384
Datum
13811385
ftoi8(PG_FUNCTION_ARGS)
13821386
{
1383-
float4 arg = PG_GETARG_FLOAT4(0);
1384-
int64 result;
1385-
float8 darg;
1386-
1387-
/* Round arg to nearest integer (but it's still in float form) */
1388-
darg = rint(arg);
1387+
float4 num = PG_GETARG_FLOAT4(0);
13891388

13901389
/*
1391-
* Does it fit in an int64? Avoid assuming that we have handy constants
1392-
* defined for the range boundaries, instead test for overflow by
1393-
* reverse-conversion.
1390+
* Get rid of any fractional part in the input. This is so we don't fail
1391+
* on just-out-of-range values that would round into range. Note
1392+
* assumption that rint() will pass through a NaN or Inf unchanged.
13941393
*/
1395-
result = (int64) darg;
1394+
num = rint(num);
13961395

1397-
if ((float8) result != darg)
1396+
/*
1397+
* Range check. We must be careful here that the boundary values are
1398+
* expressed exactly in the float domain. We expect PG_INT64_MIN to be an
1399+
* exact power of 2, so it will be represented exactly; but PG_INT64_MAX
1400+
* isn't, and might get rounded off, so avoid using it.
1401+
*/
1402+
if (num < (float4) PG_INT64_MIN ||
1403+
num >= -((float4) PG_INT64_MIN) ||
1404+
isnan(num))
13981405
ereport(ERROR,
13991406
(errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),
14001407
errmsg("bigint out of range")));
14011408

1402-
PG_RETURN_INT64(result);
1409+
PG_RETURN_INT64((int64) num);
14031410
}
14041411

14051412
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
@@ -442,6 +442,55 @@ SELECT '' AS five, * FROM FLOAT8_TBL;
442442
| -1.2345678901234e-200
443443
(5 rows)
444444

445+
-- test edge-case coercions to integer
446+
SELECT '32767.4'::float8::int2;
447+
int2
448+
-------
449+
32767
450+
(1 row)
451+
452+
SELECT '32767.6'::float8::int2;
453+
ERROR: smallint out of range
454+
SELECT '-32768.4'::float8::int2;
455+
int2
456+
--------
457+
-32768
458+
(1 row)
459+
460+
SELECT '-32768.6'::float8::int2;
461+
ERROR: smallint out of range
462+
SELECT '2147483647.4'::float8::int4;
463+
int4
464+
------------
465+
2147483647
466+
(1 row)
467+
468+
SELECT '2147483647.6'::float8::int4;
469+
ERROR: integer out of range
470+
SELECT '-2147483648.4'::float8::int4;
471+
int4
472+
-------------
473+
-2147483648
474+
(1 row)
475+
476+
SELECT '-2147483648.6'::float8::int4;
477+
ERROR: integer out of range
478+
SELECT '9223372036854773760'::float8::int8;
479+
int8
480+
---------------------
481+
9223372036854773760
482+
(1 row)
483+
484+
SELECT '9223372036854775807'::float8::int8;
485+
ERROR: bigint out of range
486+
SELECT '-9223372036854775808.5'::float8::int8;
487+
int8
488+
----------------------
489+
-9223372036854775808
490+
(1 row)
491+
492+
SELECT '-9223372036854780000'::float8::int8;
493+
ERROR: bigint out of range
445494
-- test exact cases for trigonometric functions in degrees
446495
SET extra_float_digits = 3;
447496
SELECT x,

src/test/regress/expected/float8.out

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

447+
-- test edge-case coercions to integer
448+
SELECT '32767.4'::float8::int2;
449+
int2
450+
-------
451+
32767
452+
(1 row)
453+
454+
SELECT '32767.6'::float8::int2;
455+
ERROR: smallint out of range
456+
SELECT '-32768.4'::float8::int2;
457+
int2
458+
--------
459+
-32768
460+
(1 row)
461+
462+
SELECT '-32768.6'::float8::int2;
463+
ERROR: smallint out of range
464+
SELECT '2147483647.4'::float8::int4;
465+
int4
466+
------------
467+
2147483647
468+
(1 row)
469+
470+
SELECT '2147483647.6'::float8::int4;
471+
ERROR: integer out of range
472+
SELECT '-2147483648.4'::float8::int4;
473+
int4
474+
-------------
475+
-2147483648
476+
(1 row)
477+
478+
SELECT '-2147483648.6'::float8::int4;
479+
ERROR: integer out of range
480+
SELECT '9223372036854773760'::float8::int8;
481+
int8
482+
---------------------
483+
9223372036854773760
484+
(1 row)
485+
486+
SELECT '9223372036854775807'::float8::int8;
487+
ERROR: bigint out of range
488+
SELECT '-9223372036854775808.5'::float8::int8;
489+
int8
490+
----------------------
491+
-9223372036854775808
492+
(1 row)
493+
494+
SELECT '-9223372036854780000'::float8::int8;
495+
ERROR: bigint out of range
447496
-- test exact cases for trigonometric functions in degrees
448497
SET extra_float_digits = 3;
449498
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)