Commit a57d312a authored by Tom Lane's avatar Tom Lane

Support infinity and -infinity in the numeric data type.

Add infinities that behave the same as they do in the floating-point
data types.  Aside from any intrinsic usefulness these may have,
this closes an important gap in our ability to convert floating
values to numeric and/or replace float-based APIs with numeric.

The new values are represented by bit patterns that were formerly
not used (although old code probably would take them for NaNs).
So there shouldn't be any pg_upgrade hazard.

Patch by me, reviewed by Dean Rasheed and Andrew Gierth

Discussion: https://postgr.es/m/606717.1591924582@sss.pgh.pa.us
parent 9e108984
......@@ -227,10 +227,8 @@ SV_to_JsonbValue(SV *in, JsonbParseState **jsonb_state, bool is_elem)
/*
* jsonb doesn't allow infinity or NaN (per JSON
* specification), but the numeric type that is used for the
* storage accepts NaN, so we have to prevent it here
* explicitly. We don't really have to check for isinf()
* here, as numeric doesn't allow it and it would be caught
* later, but it makes for a nicer error message.
* storage accepts those, so we have to reject them here
* explicitly.
*/
if (isinf(nval))
ereport(ERROR,
......
......@@ -387,14 +387,17 @@ PLyNumber_ToJsonbValue(PyObject *obj, JsonbValue *jbvNum)
pfree(str);
/*
* jsonb doesn't allow NaN (per JSON specification), so we have to prevent
* it here explicitly. (Infinity is also not allowed in jsonb, but
* numeric_in above already catches that.)
* jsonb doesn't allow NaN or infinity (per JSON specification), so we
* have to reject those here explicitly.
*/
if (numeric_is_nan(num))
ereport(ERROR,
(errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),
errmsg("cannot convert NaN to jsonb")));
if (numeric_is_inf(num))
ereport(ERROR,
(errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),
errmsg("cannot convert infinity to jsonb")));
jbvNum->type = jbvNumeric;
jbvNum->val.numeric = num;
......
......@@ -554,9 +554,9 @@ NUMERIC(<replaceable>precision</replaceable>)
<programlisting>
NUMERIC
</programlisting>
without any precision or scale creates a column in which numeric
values of any precision and scale can be stored, up to the
implementation limit on precision. A column of this kind will
without any precision or scale creates an <quote>unconstrained
numeric</quote> column in which numeric values of any length can be
stored, up to the implementation limits. A column of this kind will
not coerce input values to any particular scale, whereas
<type>numeric</type> columns with a declared scale will coerce
input values to that scale. (The <acronym>SQL</acronym> standard
......@@ -568,10 +568,10 @@ NUMERIC
<note>
<para>
The maximum allowed precision when explicitly specified in the
type declaration is 1000; <type>NUMERIC</type> without a specified
precision is subject to the limits described in <xref
linkend="datatype-numeric-table"/>.
The maximum precision that can be explicitly specified in
a <type>NUMERIC</type> type declaration is 1000. An
unconstrained <type>NUMERIC</type> column is subject to the limits
described in <xref linkend="datatype-numeric-table"/>.
</para>
</note>
......@@ -593,6 +593,11 @@ NUMERIC
plus three to eight bytes overhead.
</para>
<indexterm>
<primary>infinity</primary>
<secondary>numeric (data type)</secondary>
</indexterm>
<indexterm>
<primary>NaN</primary>
<see>not a number</see>
......@@ -604,13 +609,44 @@ NUMERIC
</indexterm>
<para>
In addition to ordinary numeric values, the <type>numeric</type>
type allows the special value <literal>NaN</literal>, meaning
<quote>not-a-number</quote>. Any operation on <literal>NaN</literal>
yields another <literal>NaN</literal>. When writing this value
as a constant in an SQL command, you must put quotes around it,
for example <literal>UPDATE table SET x = 'NaN'</literal>. On input,
the string <literal>NaN</literal> is recognized in a case-insensitive manner.
In addition to ordinary numeric values, the <type>numeric</type> type
has several special values:
<literallayout>
<literal>Infinity</literal>
<literal>-Infinity</literal>
<literal>NaN</literal>
</literallayout>
These are adapted from the IEEE 754 standard, and represent
<quote>infinity</quote>, <quote>negative infinity</quote>, and
<quote>not-a-number</quote>, respectively. When writing these values
as constants in an SQL command, you must put quotes around them,
for example <literal>UPDATE table SET x = '-Infinity'</literal>.
On input, these strings are recognized in a case-insensitive manner.
The infinity values can alternatively be spelled <literal>inf</literal>
and <literal>-inf</literal>.
</para>
<para>
The infinity values behave as per mathematical expectations. For
example, <literal>Infinity</literal> plus any finite value equals
<literal>Infinity</literal>, as does <literal>Infinity</literal>
plus <literal>Infinity</literal>; but <literal>Infinity</literal>
minus <literal>Infinity</literal> yields <literal>NaN</literal> (not a
number), because it has no well-defined interpretation. Note that an
infinity can only be stored in an unconstrained <type>numeric</type>
column, because it notionally exceeds any finite precision limit.
</para>
<para>
The <literal>NaN</literal> (not a number) value is used to represent
undefined calculational results. In general, any operation with
a <literal>NaN</literal> input yields another <literal>NaN</literal>.
The only exception is when the operation's other inputs are such that
the same output would be obtained if the <literal>NaN</literal> were to
be replaced by any finite or infinite numeric value; then, that output
value is used for <literal>NaN</literal> too. (An example of this
principle is that <literal>NaN</literal> raised to the zero power
yields one.)
</para>
<note>
......@@ -781,9 +817,14 @@ FROM generate_series(-3.5, 3.5, 1) as x;
</para>
</note>
<indexterm>
<primary>infinity</primary>
<secondary>floating point</secondary>
</indexterm>
<indexterm>
<primary>not a number</primary>
<secondary>double precision</secondary>
<secondary>floating point</secondary>
</indexterm>
<para>
......@@ -800,11 +841,13 @@ FROM generate_series(-3.5, 3.5, 1) as x;
as constants in an SQL command, you must put quotes around them,
for example <literal>UPDATE table SET x = '-Infinity'</literal>. On input,
these strings are recognized in a case-insensitive manner.
The infinity values can alternatively be spelled <literal>inf</literal>
and <literal>-inf</literal>.
</para>
<note>
<para>
IEEE754 specifies that <literal>NaN</literal> should not compare equal
IEEE 754 specifies that <literal>NaN</literal> should not compare equal
to any other floating-point value (including <literal>NaN</literal>).
In order to allow floating-point values to be sorted and used
in tree-based indexes, <productname>PostgreSQL</productname> treats
......
......@@ -6129,9 +6129,12 @@ numeric_to_char(PG_FUNCTION_ARGS)
/*
* numeric_out_sci() does not emit a sign for positive numbers. We
* need to add a space in this case so that positive and negative
* numbers are aligned. We also have to do the right thing for NaN.
* numbers are aligned. Also must check for NaN/infinity cases, which
* we handle the same way as in float8_to_char.
*/
if (strcmp(orgnum, "NaN") == 0)
if (strcmp(orgnum, "NaN") == 0 ||
strcmp(orgnum, "Infinity") == 0 ||
strcmp(orgnum, "-Infinity") == 0)
{
/*
* Allow 6 characters for the leading sign, the decimal point,
......@@ -6346,7 +6349,7 @@ int8_to_char(PG_FUNCTION_ARGS)
/*
* numeric_out_sci() does not emit a sign for positive numbers. We
* need to add a space in this case so that positive and negative
* numbers are aligned. We don't have to worry about NaN here.
* numbers are aligned. We don't have to worry about NaN/inf here.
*/
if (*orgnum != '-')
{
......
This diff is collapsed.
......@@ -57,6 +57,7 @@ typedef struct NumericData *Numeric;
* Utility functions in numeric.c
*/
extern bool numeric_is_nan(Numeric num);
extern bool numeric_is_inf(Numeric num);
int32 numeric_maximum_size(int32 typmod);
extern char *numeric_out_sci(Numeric num, int scale);
extern char *numeric_normalize(Numeric num);
......
......@@ -211,6 +211,18 @@ SELECT stddev_pop(3.0::numeric), stddev_samp(4.0::numeric);
0 |
(1 row)
SELECT var_pop('inf'::numeric), var_samp('inf'::numeric);
var_pop | var_samp
---------+----------
NaN |
(1 row)
SELECT stddev_pop('inf'::numeric), stddev_samp('inf'::numeric);
stddev_pop | stddev_samp
------------+-------------
NaN |
(1 row)
SELECT var_pop('nan'::numeric), var_samp('nan'::numeric);
var_pop | var_samp
---------+----------
......@@ -285,32 +297,74 @@ select avg('NaN'::numeric) from generate_series(1,3);
(1 row)
-- verify correct results for infinite inputs
SELECT avg(x::float8), var_pop(x::float8)
SELECT sum(x::float8), avg(x::float8), var_pop(x::float8)
FROM (VALUES ('1'), ('infinity')) v(x);
avg | var_pop
----------+---------
Infinity | NaN
sum | avg | var_pop
----------+----------+---------
Infinity | Infinity | NaN
(1 row)
SELECT avg(x::float8), var_pop(x::float8)
SELECT sum(x::float8), avg(x::float8), var_pop(x::float8)
FROM (VALUES ('infinity'), ('1')) v(x);
avg | var_pop
----------+---------
Infinity | NaN
sum | avg | var_pop
----------+----------+---------
Infinity | Infinity | NaN
(1 row)
SELECT avg(x::float8), var_pop(x::float8)
SELECT sum(x::float8), avg(x::float8), var_pop(x::float8)
FROM (VALUES ('infinity'), ('infinity')) v(x);
avg | var_pop
----------+---------
Infinity | NaN
sum | avg | var_pop
----------+----------+---------
Infinity | Infinity | NaN
(1 row)
SELECT avg(x::float8), var_pop(x::float8)
SELECT sum(x::float8), avg(x::float8), var_pop(x::float8)
FROM (VALUES ('-infinity'), ('infinity')) v(x);
sum | avg | var_pop
-----+-----+---------
NaN | NaN | NaN
(1 row)
SELECT sum(x::float8), avg(x::float8), var_pop(x::float8)
FROM (VALUES ('-infinity'), ('-infinity')) v(x);
sum | avg | var_pop
-----------+-----------+---------
-Infinity | -Infinity | NaN
(1 row)
SELECT sum(x::numeric), avg(x::numeric), var_pop(x::numeric)
FROM (VALUES ('1'), ('infinity')) v(x);
sum | avg | var_pop
----------+----------+---------
Infinity | Infinity | NaN
(1 row)
SELECT sum(x::numeric), avg(x::numeric), var_pop(x::numeric)
FROM (VALUES ('infinity'), ('1')) v(x);
sum | avg | var_pop
----------+----------+---------
Infinity | Infinity | NaN
(1 row)
SELECT sum(x::numeric), avg(x::numeric), var_pop(x::numeric)
FROM (VALUES ('infinity'), ('infinity')) v(x);
sum | avg | var_pop
----------+----------+---------
Infinity | Infinity | NaN
(1 row)
SELECT sum(x::numeric), avg(x::numeric), var_pop(x::numeric)
FROM (VALUES ('-infinity'), ('infinity')) v(x);
avg | var_pop
-----+---------
NaN | NaN
sum | avg | var_pop
-----+-----+---------
NaN | NaN | NaN
(1 row)
SELECT sum(x::numeric), avg(x::numeric), var_pop(x::numeric)
FROM (VALUES ('-infinity'), ('-infinity')) v(x);
sum | avg | var_pop
-----------+-----------+---------
-Infinity | -Infinity | NaN
(1 row)
-- test accuracy with a large input offset
......
This diff is collapsed.
......@@ -1872,7 +1872,7 @@ create temp table numerics(
f_numeric numeric
);
insert into numerics values
(0, '-infinity', '-infinity', '-1000'), -- numeric type lacks infinities
(0, '-infinity', '-infinity', '-infinity'),
(1, -3, -3, -3),
(2, -1, -1, -1),
(3, 0, 0, 0),
......@@ -1880,7 +1880,7 @@ insert into numerics values
(5, 1.12, 1.12, 1.12),
(6, 2, 2, 2),
(7, 100, 100, 100),
(8, 'infinity', 'infinity', '1000'),
(8, 'infinity', 'infinity', 'infinity'),
(9, 'NaN', 'NaN', 'NaN');
select id, f_float4, first_value(id) over w, last_value(id) over w
from numerics
......@@ -2078,7 +2078,7 @@ window w as (order by f_numeric range between
1 preceding and 1 following);
id | f_numeric | first_value | last_value
----+-----------+-------------+------------
0 | -1000 | 0 | 0
0 | -Infinity | 0 | 0
1 | -3 | 1 | 1
2 | -1 | 2 | 3
3 | 0 | 2 | 3
......@@ -2086,7 +2086,7 @@ window w as (order by f_numeric range between
5 | 1.12 | 4 | 6
6 | 2 | 4 | 6
7 | 100 | 7 | 7
8 | 1000 | 8 | 8
8 | Infinity | 8 | 8
9 | NaN | 9 | 9
(10 rows)
......@@ -2096,7 +2096,7 @@ window w as (order by f_numeric range between
1 preceding and 1.1::numeric following);
id | f_numeric | first_value | last_value
----+-----------+-------------+------------
0 | -1000 | 0 | 0
0 | -Infinity | 0 | 0
1 | -3 | 1 | 1
2 | -1 | 2 | 3
3 | 0 | 2 | 4
......@@ -2104,7 +2104,7 @@ window w as (order by f_numeric range between
5 | 1.12 | 4 | 6
6 | 2 | 4 | 6
7 | 100 | 7 | 7
8 | 1000 | 8 | 8
8 | Infinity | 8 | 8
9 | NaN | 9 | 9
(10 rows)
......@@ -2116,6 +2116,60 @@ ERROR: RANGE with offset PRECEDING/FOLLOWING is not supported for column type n
LINE 4: 1 preceding and 1.1::float8 following);
^
HINT: Cast the offset value to an appropriate type.
select id, f_numeric, first_value(id) over w, last_value(id) over w
from numerics
window w as (order by f_numeric range between
'inf' preceding and 'inf' following);
id | f_numeric | first_value | last_value
----+-----------+-------------+------------
0 | -Infinity | 0 | 8
1 | -3 | 0 | 8
2 | -1 | 0 | 8
3 | 0 | 0 | 8
4 | 1.1 | 0 | 8
5 | 1.12 | 0 | 8
6 | 2 | 0 | 8
7 | 100 | 0 | 8
8 | Infinity | 0 | 8
9 | NaN | 9 | 9
(10 rows)
select id, f_numeric, first_value(id) over w, last_value(id) over w
from numerics
window w as (order by f_numeric range between
'inf' preceding and 'inf' preceding);
id | f_numeric | first_value | last_value
----+-----------+-------------+------------
0 | -Infinity | 0 | 0
1 | -3 | 0 | 0
2 | -1 | 0 | 0
3 | 0 | 0 | 0
4 | 1.1 | 0 | 0
5 | 1.12 | 0 | 0
6 | 2 | 0 | 0
7 | 100 | 0 | 0
8 | Infinity | 0 | 8
9 | NaN | 9 | 9
(10 rows)
select id, f_numeric, first_value(id) over w, last_value(id) over w
from numerics
window w as (order by f_numeric range between
'inf' following and 'inf' following);
id | f_numeric | first_value | last_value
----+-----------+-------------+------------
0 | -Infinity | 0 | 8
1 | -3 | 8 | 8
2 | -1 | 8 | 8
3 | 0 | 8 | 8
4 | 1.1 | 8 | 8
5 | 1.12 | 8 | 8
6 | 2 | 8 | 8
7 | 100 | 8 | 8
8 | Infinity | 8 | 8
9 | NaN | 9 | 9
(10 rows)
select id, f_numeric, first_value(id) over w, last_value(id) over w
from numerics
window w as (order by f_numeric range between
......
......@@ -53,6 +53,8 @@ SELECT var_pop('nan'::float4), var_samp('nan'::float4);
SELECT stddev_pop('nan'::float4), stddev_samp('nan'::float4);
SELECT var_pop(1.0::numeric), var_samp(2.0::numeric);
SELECT stddev_pop(3.0::numeric), stddev_samp(4.0::numeric);
SELECT var_pop('inf'::numeric), var_samp('inf'::numeric);
SELECT stddev_pop('inf'::numeric), stddev_samp('inf'::numeric);
SELECT var_pop('nan'::numeric), var_samp('nan'::numeric);
SELECT stddev_pop('nan'::numeric), stddev_samp('nan'::numeric);
......@@ -69,14 +71,26 @@ select sum('NaN'::numeric) from generate_series(1,3);
select avg('NaN'::numeric) from generate_series(1,3);
-- verify correct results for infinite inputs
SELECT avg(x::float8), var_pop(x::float8)
SELECT sum(x::float8), avg(x::float8), var_pop(x::float8)
FROM (VALUES ('1'), ('infinity')) v(x);
SELECT avg(x::float8), var_pop(x::float8)
SELECT sum(x::float8), avg(x::float8), var_pop(x::float8)
FROM (VALUES ('infinity'), ('1')) v(x);
SELECT avg(x::float8), var_pop(x::float8)
SELECT sum(x::float8), avg(x::float8), var_pop(x::float8)
FROM (VALUES ('infinity'), ('infinity')) v(x);
SELECT avg(x::float8), var_pop(x::float8)
SELECT sum(x::float8), avg(x::float8), var_pop(x::float8)
FROM (VALUES ('-infinity'), ('infinity')) v(x);
SELECT sum(x::float8), avg(x::float8), var_pop(x::float8)
FROM (VALUES ('-infinity'), ('-infinity')) v(x);
SELECT sum(x::numeric), avg(x::numeric), var_pop(x::numeric)
FROM (VALUES ('1'), ('infinity')) v(x);
SELECT sum(x::numeric), avg(x::numeric), var_pop(x::numeric)
FROM (VALUES ('infinity'), ('1')) v(x);
SELECT sum(x::numeric), avg(x::numeric), var_pop(x::numeric)
FROM (VALUES ('infinity'), ('infinity')) v(x);
SELECT sum(x::numeric), avg(x::numeric), var_pop(x::numeric)
FROM (VALUES ('-infinity'), ('infinity')) v(x);
SELECT sum(x::numeric), avg(x::numeric), var_pop(x::numeric)
FROM (VALUES ('-infinity'), ('-infinity')) v(x);
-- test accuracy with a large input offset
SELECT avg(x::float8), var_pop(x::float8)
......
This diff is collapsed.
......@@ -499,7 +499,7 @@ create temp table numerics(
);
insert into numerics values
(0, '-infinity', '-infinity', '-1000'), -- numeric type lacks infinities
(0, '-infinity', '-infinity', '-infinity'),
(1, -3, -3, -3),
(2, -1, -1, -1),
(3, 0, 0, 0),
......@@ -507,7 +507,7 @@ insert into numerics values
(5, 1.12, 1.12, 1.12),
(6, 2, 2, 2),
(7, 100, 100, 100),
(8, 'infinity', 'infinity', '1000'),
(8, 'infinity', 'infinity', 'infinity'),
(9, 'NaN', 'NaN', 'NaN');
select id, f_float4, first_value(id) over w, last_value(id) over w
......@@ -574,6 +574,18 @@ window w as (order by f_numeric range between
1 preceding and 1.1::float8 following); -- currently unsupported
select id, f_numeric, first_value(id) over w, last_value(id) over w
from numerics
window w as (order by f_numeric range between
'inf' preceding and 'inf' following);
select id, f_numeric, first_value(id) over w, last_value(id) over w
from numerics
window w as (order by f_numeric range between
'inf' preceding and 'inf' preceding);
select id, f_numeric, first_value(id) over w, last_value(id) over w
from numerics
window w as (order by f_numeric range between
'inf' following and 'inf' following);
select id, f_numeric, first_value(id) over w, last_value(id) over w
from numerics
window w as (order by f_numeric range between
1.1 preceding and 'NaN' following); -- error, NaN disallowed
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment