-- Use terse verbosity, so that the expected output does not depend on the -- error context, which differs between PostgreSQL versions (particularly -- for the COPY ... FROM statements below). \set VERBOSITY terse -- Reproducer: tdigest_trimmed_agg() accumulated the trimmed sum by adding -- (mean * count) for all the centroids in the range, and the trimmed average -- was then calculated by dividing that sum by the number of items. -- -- But the sum may overflow to infinity even when the average is perfectly -- representable. The average of 1e307 values is 1e307, yet the sum of a -- hundred of them is not a float8. The average was then reported as -- Infinity, which is simply a wrong answer - the average of a digest is -- always within the range of the centroid means, so it can never overflow. -- -- And once one partial sum saturates to +Infinity and another one to -- -Infinity, the accumulator turns into NaN, so a digest with large values -- of both signs reported NaN for both the sum and the average. -- -- The sum, unlike the average, may legitimately exceed the float8 range. -- Report that as an error, just like the regular float8 arithmetic does, -- instead of silently returning infinity. -- -- query before after -- tdigest_digest_avg((1e307, 100)) Infinity 1e+307 -- tdigest_digest_sum((1e307, 100)) Infinity ERROR -- tdigest_digest_avg((-1e308,2) (1e308,2)) NaN 0 -- tdigest_digest_sum((-1e308,2) (1e308,2)) NaN 0 -- -- This affects tdigest_digest_sum() and tdigest_digest_avg() as well as the -- tdigest_sum() and tdigest_avg() aggregates, i.e. anything going through -- tdigest_trimmed_agg(). -- The average is representable, the sum is not. The average has to be -- 1e307, the sum has to be reported as an overflow. SELECT tdigest_digest_avg('flags 1 count 100 compression 10 centroids 1 (1e307, 100)'::tdigest); SELECT tdigest_digest_sum('flags 1 count 100 compression 10 centroids 1 (1e307, 100)'::tdigest); -- Trimming does not change that, as long as enough items remain. SELECT tdigest_digest_avg('flags 1 count 100 compression 10 centroids 1 (1e307, 100)'::tdigest, 0.25, 0.75); SELECT tdigest_digest_sum('flags 1 count 100 compression 10 centroids 1 (1e307, 100)'::tdigest, 0.25, 0.75); -- Trim it down to a single item, and even the sum fits again. SELECT tdigest_digest_avg('flags 1 count 100 compression 10 centroids 1 (1e307, 100)'::tdigest, 0.5, 0.51); SELECT tdigest_digest_sum('flags 1 count 100 compression 10 centroids 1 (1e307, 100)'::tdigest, 0.5, 0.51); -- Large values of both signs, cancelling out exactly. Both the average and -- the sum are 0, the accumulator used to end up as NaN. SELECT tdigest_digest_avg('flags 1 count 4 compression 10 centroids 2 (-1e308, 2) (1e308, 2)'::tdigest); SELECT tdigest_digest_sum('flags 1 count 4 compression 10 centroids 2 (-1e308, 2) (1e308, 2)'::tdigest); -- The same, but with the values cancelling only partially. SELECT tdigest_digest_avg('flags 1 count 4 compression 10 centroids 2 (-1e308, 1) (1e308, 3)'::tdigest); SELECT tdigest_digest_sum('flags 1 count 4 compression 10 centroids 2 (-1e308, 1) (1e308, 3)'::tdigest); -- Trim the negative values away, and only the overflowing part remains. SELECT tdigest_digest_avg('flags 1 count 4 compression 10 centroids 2 (-1e308, 2) (1e308, 2)'::tdigest, 0.5, 1.0); SELECT tdigest_digest_sum('flags 1 count 4 compression 10 centroids 2 (-1e308, 2) (1e308, 2)'::tdigest, 0.5, 1.0); -- The trimmed aggregates on the value/count API go through a different code -- path (the aggregate final functions), so check those too. SELECT tdigest_avg(1e307::float8, 200::bigint, 10, 0.0, 1.0); SELECT tdigest_sum(1e307::float8, 200::bigint, 10, 0.0, 1.0); -- And the plain value API. SELECT tdigest_avg(v, 10, 0.0, 1.0) FROM (VALUES (1e308::float8), (1e308::float8)) t(v); SELECT tdigest_sum(v, 10, 0.0, 1.0) FROM (VALUES (1e308::float8), (1e308::float8)) t(v); SELECT tdigest_avg(v, 10, 0.0, 1.0) FROM (VALUES (-1e308::float8), (1e308::float8)) t(v); SELECT tdigest_sum(v, 10, 0.0, 1.0) FROM (VALUES (-1e308::float8), (1e308::float8)) t(v); -- Ordinary values are not affected in any way, and an empty range still -- returns NULL. SELECT tdigest_digest_avg('flags 1 count 30 compression 10 centroids 3 (10, 10) (20, 5) (30, 15)'::tdigest, 0.1, 0.9); SELECT tdigest_digest_sum('flags 1 count 30 compression 10 centroids 3 (10, 10) (20, 5) (30, 15)'::tdigest, 0.1, 0.9); SELECT tdigest_digest_avg('flags 1 count 30 compression 10 centroids 3 (10, 10) (20, 5) (30, 15)'::tdigest, 0.5, 0.5); SELECT tdigest_digest_sum('flags 1 count 30 compression 10 centroids 3 (10, 10) (20, 5) (30, 15)'::tdigest, 0.5, 0.5);