From 0bc3b42b5c8a44adbf7d4579c1e62a6f3d3f55e0 Mon Sep 17 00:00:00 2001 From: Ayush Tiwari Date: Wed, 4 Nov 2026 19:05:19 +0530 Subject: [PATCH v2 1/2] Fix NaN handling in cube Cube's comparison functions use raw floating-point comparisons, so a NaN coordinate can compare equal to a finite value. Min and Max also make the selected bounds depend on the order of the corners. Use the float8 comparison helpers instead, following PostgreSQL's convention that NaNs compare equal and sort after all other values. The distance functions can also treat a NaN coordinate as an overlapping projection and return a finite distance. Return NaN for such projections and preserve it when computing the Chebyshev maximum. Existing B-tree and GiST indexes containing NaN cube coordinates need to be reindexed. Bug: #19603 Reported-by: Yuelin Wang <1217816127@qq.com> Reported-by: Aleksander Alekseev Author: Ayush Tiwari Discussion: https://postgr.es/m/19603-7b1f783d5bbfe791@postgresql.org --- contrib/cube/cube.c | 111 ++++++++++++++++++--------------- contrib/cube/expected/cube.out | 15 +++++ contrib/cube/sql/cube.sql | 7 +++ doc/src/sgml/cube.sgml | 7 +++ 4 files changed, 88 insertions(+), 52 deletions(-) diff --git a/contrib/cube/cube.c b/contrib/cube/cube.c index 28d48549b36..7576c0a9514 100644 --- a/contrib/cube/cube.c +++ b/contrib/cube/cube.c @@ -779,20 +779,18 @@ cube_union_v0(NDBOX *a, NDBOX *b) /* First compute the union of the dimensions present in both args */ for (i = 0; i < DIM(b); i++) { - result->x[i] = Min(Min(LL_COORD(a, i), UR_COORD(a, i)), - Min(LL_COORD(b, i), UR_COORD(b, i))); - result->x[i + DIM(a)] = Max(Max(LL_COORD(a, i), UR_COORD(a, i)), - Max(LL_COORD(b, i), UR_COORD(b, i))); + result->x[i] = float8_min(float8_min(LL_COORD(a, i), UR_COORD(a, i)), + float8_min(LL_COORD(b, i), UR_COORD(b, i))); + result->x[i + DIM(a)] = float8_max(float8_max(LL_COORD(a, i), UR_COORD(a, i)), + float8_max(LL_COORD(b, i), UR_COORD(b, i))); } /* continue on the higher dimensions only present in 'a' */ for (; i < DIM(a); i++) { - result->x[i] = Min(0, - Min(LL_COORD(a, i), UR_COORD(a, i)) - ); - result->x[i + dim] = Max(0, - Max(LL_COORD(a, i), UR_COORD(a, i)) - ); + result->x[i] = float8_min(0.0, + float8_min(LL_COORD(a, i), UR_COORD(a, i))); + result->x[i + dim] = float8_max(0.0, + float8_max(LL_COORD(a, i), UR_COORD(a, i))); } /* @@ -854,20 +852,18 @@ cube_inter(PG_FUNCTION_ARGS) /* First compute intersection of the dimensions present in both args */ for (i = 0; i < DIM(b); i++) { - result->x[i] = Max(Min(LL_COORD(a, i), UR_COORD(a, i)), - Min(LL_COORD(b, i), UR_COORD(b, i))); - result->x[i + DIM(a)] = Min(Max(LL_COORD(a, i), UR_COORD(a, i)), - Max(LL_COORD(b, i), UR_COORD(b, i))); + result->x[i] = float8_max(float8_min(LL_COORD(a, i), UR_COORD(a, i)), + float8_min(LL_COORD(b, i), UR_COORD(b, i))); + result->x[i + DIM(a)] = float8_min(float8_max(LL_COORD(a, i), UR_COORD(a, i)), + float8_max(LL_COORD(b, i), UR_COORD(b, i))); } /* continue on the higher dimensions only present in 'a' */ for (; i < DIM(a); i++) { - result->x[i] = Max(0, - Min(LL_COORD(a, i), UR_COORD(a, i)) - ); - result->x[i + DIM(a)] = Min(0, - Max(LL_COORD(a, i), UR_COORD(a, i)) - ); + result->x[i] = float8_max(0.0, + float8_min(LL_COORD(a, i), UR_COORD(a, i))); + result->x[i + DIM(a)] = float8_min(0.0, + float8_max(LL_COORD(a, i), UR_COORD(a, i))); } /* @@ -951,20 +947,20 @@ cube_cmp_v0(NDBOX *a, NDBOX *b) /* compare the common dimensions */ for (i = 0; i < dim; i++) { - if (Min(LL_COORD(a, i), UR_COORD(a, i)) > - Min(LL_COORD(b, i), UR_COORD(b, i))) + if (float8_gt(float8_min(LL_COORD(a, i), UR_COORD(a, i)), + float8_min(LL_COORD(b, i), UR_COORD(b, i)))) return 1; - if (Min(LL_COORD(a, i), UR_COORD(a, i)) < - Min(LL_COORD(b, i), UR_COORD(b, i))) + if (float8_lt(float8_min(LL_COORD(a, i), UR_COORD(a, i)), + float8_min(LL_COORD(b, i), UR_COORD(b, i)))) return -1; } for (i = 0; i < dim; i++) { - if (Max(LL_COORD(a, i), UR_COORD(a, i)) > - Max(LL_COORD(b, i), UR_COORD(b, i))) + if (float8_gt(float8_max(LL_COORD(a, i), UR_COORD(a, i)), + float8_max(LL_COORD(b, i), UR_COORD(b, i)))) return 1; - if (Max(LL_COORD(a, i), UR_COORD(a, i)) < - Max(LL_COORD(b, i), UR_COORD(b, i))) + if (float8_lt(float8_max(LL_COORD(a, i), UR_COORD(a, i)), + float8_max(LL_COORD(b, i), UR_COORD(b, i)))) return -1; } @@ -973,16 +969,16 @@ cube_cmp_v0(NDBOX *a, NDBOX *b) { for (i = dim; i < DIM(a); i++) { - if (Min(LL_COORD(a, i), UR_COORD(a, i)) > 0) + if (float8_gt(float8_min(LL_COORD(a, i), UR_COORD(a, i)), 0.0)) return 1; - if (Min(LL_COORD(a, i), UR_COORD(a, i)) < 0) + if (float8_lt(float8_min(LL_COORD(a, i), UR_COORD(a, i)), 0.0)) return -1; } for (i = dim; i < DIM(a); i++) { - if (Max(LL_COORD(a, i), UR_COORD(a, i)) > 0) + if (float8_gt(float8_max(LL_COORD(a, i), UR_COORD(a, i)), 0.0)) return 1; - if (Max(LL_COORD(a, i), UR_COORD(a, i)) < 0) + if (float8_lt(float8_max(LL_COORD(a, i), UR_COORD(a, i)), 0.0)) return -1; } @@ -996,16 +992,16 @@ cube_cmp_v0(NDBOX *a, NDBOX *b) { for (i = dim; i < DIM(b); i++) { - if (Min(LL_COORD(b, i), UR_COORD(b, i)) > 0) + if (float8_gt(float8_min(LL_COORD(b, i), UR_COORD(b, i)), 0.0)) return -1; - if (Min(LL_COORD(b, i), UR_COORD(b, i)) < 0) + if (float8_lt(float8_min(LL_COORD(b, i), UR_COORD(b, i)), 0.0)) return 1; } for (i = dim; i < DIM(b); i++) { - if (Max(LL_COORD(b, i), UR_COORD(b, i)) > 0) + if (float8_gt(float8_max(LL_COORD(b, i), UR_COORD(b, i)), 0.0)) return -1; - if (Max(LL_COORD(b, i), UR_COORD(b, i)) < 0) + if (float8_lt(float8_max(LL_COORD(b, i), UR_COORD(b, i)), 0.0)) return 1; } @@ -1154,11 +1150,11 @@ cube_contains_v0(NDBOX *a, NDBOX *b) /* Can't care less about the excess dimensions of (a), if any */ for (i = 0; i < Min(DIM(a), DIM(b)); i++) { - if (Min(LL_COORD(a, i), UR_COORD(a, i)) > - Min(LL_COORD(b, i), UR_COORD(b, i))) + if (float8_gt(float8_min(LL_COORD(a, i), UR_COORD(a, i)), + float8_min(LL_COORD(b, i), UR_COORD(b, i)))) return false; - if (Max(LL_COORD(a, i), UR_COORD(a, i)) < - Max(LL_COORD(b, i), UR_COORD(b, i))) + if (float8_lt(float8_max(LL_COORD(a, i), UR_COORD(a, i)), + float8_max(LL_COORD(b, i), UR_COORD(b, i)))) return false; } @@ -1217,18 +1213,20 @@ cube_overlap_v0(NDBOX *a, NDBOX *b) /* compare within the dimensions of (b) */ for (i = 0; i < DIM(b); i++) { - if (Min(LL_COORD(a, i), UR_COORD(a, i)) > Max(LL_COORD(b, i), UR_COORD(b, i))) + if (float8_gt(float8_min(LL_COORD(a, i), UR_COORD(a, i)), + float8_max(LL_COORD(b, i), UR_COORD(b, i)))) return false; - if (Max(LL_COORD(a, i), UR_COORD(a, i)) < Min(LL_COORD(b, i), UR_COORD(b, i))) + if (float8_lt(float8_max(LL_COORD(a, i), UR_COORD(a, i)), + float8_min(LL_COORD(b, i), UR_COORD(b, i)))) return false; } /* compare to zero those dimensions in (a) absent in (b) */ for (i = DIM(b); i < DIM(a); i++) { - if (Min(LL_COORD(a, i), UR_COORD(a, i)) > 0) + if (float8_gt(float8_min(LL_COORD(a, i), UR_COORD(a, i)), 0.0)) return false; - if (Max(LL_COORD(a, i), UR_COORD(a, i)) < 0) + if (float8_lt(float8_max(LL_COORD(a, i), UR_COORD(a, i)), 0.0)) return false; } @@ -1377,7 +1375,7 @@ distance_chebyshev(PG_FUNCTION_ARGS) { d = fabs(distance_1D(LL_COORD(a, i), UR_COORD(a, i), LL_COORD(b, i), UR_COORD(b, i))); - if (d > distance) + if (isnan(d) || d > distance) distance = d; } @@ -1385,7 +1383,7 @@ distance_chebyshev(PG_FUNCTION_ARGS) for (i = DIM(b); i < DIM(a); i++) { d = fabs(distance_1D(LL_COORD(a, i), UR_COORD(a, i), 0.0, 0.0)); - if (d > distance) + if (isnan(d) || d > distance) distance = d; } @@ -1511,6 +1509,15 @@ g_cube_distance(PG_FUNCTION_ARGS) static double distance_1D(double a1, double a2, double b1, double b2) { + /* + * If any endpoint is NaN, all the comparisons below are false and we'd + * fall through to the "intersecting" case and return 0. Return NaN + * instead, so that callers propagate it as ordinary float arithmetic + * does. + */ + if (isnan(a1) || isnan(a2) || isnan(b1) || isnan(b2)) + return get_float8_nan(); + /* interval (a) is entirely on the left of (b) */ if ((a1 <= b1) && (a2 <= b1) && (a1 <= b2) && (a2 <= b2)) return (Min(b1, b2) - Max(a1, a2)); @@ -1578,7 +1585,7 @@ cube_ll_coord(PG_FUNCTION_ARGS) double result; if (DIM(c) >= n && n > 0) - result = Min(LL_COORD(c, n - 1), UR_COORD(c, n - 1)); + result = float8_min(LL_COORD(c, n - 1), UR_COORD(c, n - 1)); else result = 0; @@ -1595,7 +1602,7 @@ cube_ur_coord(PG_FUNCTION_ARGS) double result; if (DIM(c) >= n && n > 0) - result = Max(LL_COORD(c, n - 1), UR_COORD(c, n - 1)); + result = float8_max(LL_COORD(c, n - 1), UR_COORD(c, n - 1)); else result = 0; @@ -1685,9 +1692,9 @@ cube_coord_llur(PG_FUNCTION_ARGS) else { if (upper) - result = Max(cube->x[index], cube->x[index + DIM(cube)]); + result = float8_max(cube->x[index], cube->x[index + DIM(cube)]); else - result = Min(cube->x[index], cube->x[index + DIM(cube)]); + result = float8_min(cube->x[index], cube->x[index + DIM(cube)]); } } else @@ -1734,7 +1741,7 @@ cube_enlarge(PG_FUNCTION_ARGS) for (i = 0, j = dim; i < DIM(a); i++, j++) { - if (LL_COORD(a, i) >= UR_COORD(a, i)) + if (float8_ge(LL_COORD(a, i), UR_COORD(a, i))) { result->x[i] = UR_COORD(a, i) - r; result->x[j] = LL_COORD(a, i) + r; @@ -1744,7 +1751,7 @@ cube_enlarge(PG_FUNCTION_ARGS) result->x[i] = LL_COORD(a, i) - r; result->x[j] = UR_COORD(a, i) + r; } - if (result->x[i] > result->x[j]) + if (float8_gt(result->x[i], result->x[j])) { result->x[i] = (result->x[i] + result->x[j]) / 2; result->x[j] = result->x[i]; diff --git a/contrib/cube/expected/cube.out b/contrib/cube/expected/cube.out index 47787c50bd9..51e50d90e55 100644 --- a/contrib/cube/expected/cube.out +++ b/contrib/cube/expected/cube.out @@ -705,6 +705,13 @@ SELECT '(2,0,0,0,0),(3,1,0,0,0)'::cube < '(2,0),(3,1)'::cube AS bool; f (1 row) +SELECT '(1,1)'::cube = '(NaN,NaN)'::cube AS eq, + '(NaN)'::cube > '(1)'::cube AS gt; + eq | gt +----+---- + f | t +(1 row) + -- "overlap" -- SELECT '1'::cube && '1'::cube AS bool; @@ -1440,6 +1447,14 @@ SELECT distance_taxicab('(2,2),(10,10)'::cube, '(0,0),(5,5)'::cube); 0 (1 row) +SELECT '(NaN,1)'::cube <-> '(2,2)'::cube AS euclidean, + '(NaN,1)'::cube <#> '(2,2)'::cube AS taxicab, + '(NaN,1)'::cube <=> '(2,2)'::cube AS chebyshev; + euclidean | taxicab | chebyshev +-----------+---------+----------- + NaN | NaN | NaN +(1 row) + -- coordinate access SELECT cube(array[10,20,30], array[40,50,60])->1; ?column? diff --git a/contrib/cube/sql/cube.sql b/contrib/cube/sql/cube.sql index eec90d21ee3..6a77075f0da 100644 --- a/contrib/cube/sql/cube.sql +++ b/contrib/cube/sql/cube.sql @@ -189,6 +189,8 @@ SELECT '(2,0,0,0,1),(3,1,0,0,0)'::cube < '(2,0),(3,1)'::cube AS bool; SELECT '(2,0,0,0,0),(3,1,0,0,0)'::cube > '(2,0),(3,1)'::cube AS bool; SELECT '(2,0,0,0,0),(3,1,0,0,0)'::cube < '(2,0),(3,1)'::cube AS bool; +SELECT '(1,1)'::cube = '(NaN,NaN)'::cube AS eq, + '(NaN)'::cube > '(1)'::cube AS gt; -- "overlap" -- @@ -356,6 +358,11 @@ SELECT '(1,1)'::cube <#> '(4,5)'::cube as d_t; SELECT cube_distance('(2,2),(10,10)'::cube, '(0,0),(5,5)'::cube); SELECT distance_chebyshev('(2,2),(10,10)'::cube, '(0,0),(5,5)'::cube); SELECT distance_taxicab('(2,2),(10,10)'::cube, '(0,0),(5,5)'::cube); + +SELECT '(NaN,1)'::cube <-> '(2,2)'::cube AS euclidean, + '(NaN,1)'::cube <#> '(2,2)'::cube AS taxicab, + '(NaN,1)'::cube <=> '(2,2)'::cube AS chebyshev; + -- coordinate access SELECT cube(array[10,20,30], array[40,50,60])->1; SELECT cube(array[40,50,60], array[10,20,30])->1; diff --git a/doc/src/sgml/cube.sgml b/doc/src/sgml/cube.sgml index a11c0cbd767..a9ada6a64cb 100644 --- a/doc/src/sgml/cube.sgml +++ b/doc/src/sgml/cube.sgml @@ -106,6 +106,13 @@ Values are stored internally as 64-bit floating point numbers. This means that numbers with more than about 16 significant digits will be truncated. + + + As with double precision, NaN coordinates + compare equal to one another and greater than all non-NaN + values. Distance calculations return NaN if either cube + has a NaN coordinate. + -- 2.34.1