Commit 7919398b authored by Peter Eisentraut's avatar Peter Eisentraut

PL/Python: Convert numeric to Decimal

The old implementation converted PostgreSQL numeric to Python float,
which was always considered a shortcoming.  Now numeric is converted to
the Python Decimal object.  Either the external cdecimal module or the
standard library decimal module are supported.

From: Szymon Guz <mabewlun@gmail.com>
From: Ronan Dunklau <rdunklau@gmail.com>
Reviewed-by: default avatarSteve Singer <steve@ssinger.info>
parent 02d2b694
...@@ -310,12 +310,23 @@ $$ LANGUAGE plpythonu; ...@@ -310,12 +310,23 @@ $$ LANGUAGE plpythonu;
<listitem> <listitem>
<para> <para>
PostgreSQL <type>real</type>, <type>double</type>, PostgreSQL <type>real</type> and <type>double</type> are converted to
and <type>numeric</type> are converted to Python <type>float</type>.
Python <type>float</type>. Note that for </para>
the <type>numeric</type> this loses information and can lead to </listitem>
incorrect results. This might be fixed in a future
release. <listitem>
<para>
PostgreSQL <type>numeric</type> is converted to
Python <type>Decimal</type>. This type is imported from
the <literal>cdecimal</literal> package if that is available.
Otherwise,
<literal>decimal.Decimal</literal> from the standard library will be
used. <literal>cdecimal</literal> is significantly faster
than <literal>decimal</literal>. In Python 3.3,
however, <literal>cdecimal</literal> has been integrated into the
standard library under the name <literal>decimal</literal>, so there is
no longer any difference.
</para> </para>
</listitem> </listitem>
......
...@@ -213,36 +213,69 @@ CONTEXT: PL/Python function "test_type_conversion_int8" ...@@ -213,36 +213,69 @@ CONTEXT: PL/Python function "test_type_conversion_int8"
(1 row) (1 row)
CREATE FUNCTION test_type_conversion_numeric(x numeric) RETURNS numeric AS $$ CREATE FUNCTION test_type_conversion_numeric(x numeric) RETURNS numeric AS $$
plpy.info(x, type(x)) # print just the class name, not the type, to avoid differences
# between decimal and cdecimal
plpy.info(x, x.__class__.__name__)
return x return x
$$ LANGUAGE plpythonu; $$ LANGUAGE plpythonu;
/* The current implementation converts numeric to float. */
SELECT * FROM test_type_conversion_numeric(100); SELECT * FROM test_type_conversion_numeric(100);
INFO: (100.0, <type 'float'>) INFO: (Decimal('100'), 'Decimal')
CONTEXT: PL/Python function "test_type_conversion_numeric" CONTEXT: PL/Python function "test_type_conversion_numeric"
test_type_conversion_numeric test_type_conversion_numeric
------------------------------ ------------------------------
100.0 100
(1 row) (1 row)
SELECT * FROM test_type_conversion_numeric(-100); SELECT * FROM test_type_conversion_numeric(-100);
INFO: (-100.0, <type 'float'>) INFO: (Decimal('-100'), 'Decimal')
CONTEXT: PL/Python function "test_type_conversion_numeric" CONTEXT: PL/Python function "test_type_conversion_numeric"
test_type_conversion_numeric test_type_conversion_numeric
------------------------------ ------------------------------
-100.0 -100
(1 row)
SELECT * FROM test_type_conversion_numeric(100.0);
INFO: (Decimal('100.0'), 'Decimal')
CONTEXT: PL/Python function "test_type_conversion_numeric"
test_type_conversion_numeric
------------------------------
100.0
(1 row)
SELECT * FROM test_type_conversion_numeric(100.00);
INFO: (Decimal('100.00'), 'Decimal')
CONTEXT: PL/Python function "test_type_conversion_numeric"
test_type_conversion_numeric
------------------------------
100.00
(1 row) (1 row)
SELECT * FROM test_type_conversion_numeric(5000000000.5); SELECT * FROM test_type_conversion_numeric(5000000000.5);
INFO: (5000000000.5, <type 'float'>) INFO: (Decimal('5000000000.5'), 'Decimal')
CONTEXT: PL/Python function "test_type_conversion_numeric" CONTEXT: PL/Python function "test_type_conversion_numeric"
test_type_conversion_numeric test_type_conversion_numeric
------------------------------ ------------------------------
5000000000.5 5000000000.5
(1 row) (1 row)
SELECT * FROM test_type_conversion_numeric(1234567890.0987654321);
INFO: (Decimal('1234567890.0987654321'), 'Decimal')
CONTEXT: PL/Python function "test_type_conversion_numeric"
test_type_conversion_numeric
------------------------------
1234567890.0987654321
(1 row)
SELECT * FROM test_type_conversion_numeric(-1234567890.0987654321);
INFO: (Decimal('-1234567890.0987654321'), 'Decimal')
CONTEXT: PL/Python function "test_type_conversion_numeric"
test_type_conversion_numeric
------------------------------
-1234567890.0987654321
(1 row)
SELECT * FROM test_type_conversion_numeric(null); SELECT * FROM test_type_conversion_numeric(null);
INFO: (None, <type 'NoneType'>) INFO: (None, 'NoneType')
CONTEXT: PL/Python function "test_type_conversion_numeric" CONTEXT: PL/Python function "test_type_conversion_numeric"
test_type_conversion_numeric test_type_conversion_numeric
------------------------------ ------------------------------
......
...@@ -213,36 +213,69 @@ CONTEXT: PL/Python function "test_type_conversion_int8" ...@@ -213,36 +213,69 @@ CONTEXT: PL/Python function "test_type_conversion_int8"
(1 row) (1 row)
CREATE FUNCTION test_type_conversion_numeric(x numeric) RETURNS numeric AS $$ CREATE FUNCTION test_type_conversion_numeric(x numeric) RETURNS numeric AS $$
plpy.info(x, type(x)) # print just the class name, not the type, to avoid differences
# between decimal and cdecimal
plpy.info(x, x.__class__.__name__)
return x return x
$$ LANGUAGE plpython3u; $$ LANGUAGE plpython3u;
/* The current implementation converts numeric to float. */
SELECT * FROM test_type_conversion_numeric(100); SELECT * FROM test_type_conversion_numeric(100);
INFO: (100.0, <class 'float'>) INFO: (Decimal('100'), 'Decimal')
CONTEXT: PL/Python function "test_type_conversion_numeric" CONTEXT: PL/Python function "test_type_conversion_numeric"
test_type_conversion_numeric test_type_conversion_numeric
------------------------------ ------------------------------
100.0 100
(1 row) (1 row)
SELECT * FROM test_type_conversion_numeric(-100); SELECT * FROM test_type_conversion_numeric(-100);
INFO: (-100.0, <class 'float'>) INFO: (Decimal('-100'), 'Decimal')
CONTEXT: PL/Python function "test_type_conversion_numeric" CONTEXT: PL/Python function "test_type_conversion_numeric"
test_type_conversion_numeric test_type_conversion_numeric
------------------------------ ------------------------------
-100.0 -100
(1 row)
SELECT * FROM test_type_conversion_numeric(100.0);
INFO: (Decimal('100.0'), 'Decimal')
CONTEXT: PL/Python function "test_type_conversion_numeric"
test_type_conversion_numeric
------------------------------
100.0
(1 row)
SELECT * FROM test_type_conversion_numeric(100.00);
INFO: (Decimal('100.00'), 'Decimal')
CONTEXT: PL/Python function "test_type_conversion_numeric"
test_type_conversion_numeric
------------------------------
100.00
(1 row) (1 row)
SELECT * FROM test_type_conversion_numeric(5000000000.5); SELECT * FROM test_type_conversion_numeric(5000000000.5);
INFO: (5000000000.5, <class 'float'>) INFO: (Decimal('5000000000.5'), 'Decimal')
CONTEXT: PL/Python function "test_type_conversion_numeric" CONTEXT: PL/Python function "test_type_conversion_numeric"
test_type_conversion_numeric test_type_conversion_numeric
------------------------------ ------------------------------
5000000000.5 5000000000.5
(1 row) (1 row)
SELECT * FROM test_type_conversion_numeric(1234567890.0987654321);
INFO: (Decimal('1234567890.0987654321'), 'Decimal')
CONTEXT: PL/Python function "test_type_conversion_numeric"
test_type_conversion_numeric
------------------------------
1234567890.0987654321
(1 row)
SELECT * FROM test_type_conversion_numeric(-1234567890.0987654321);
INFO: (Decimal('-1234567890.0987654321'), 'Decimal')
CONTEXT: PL/Python function "test_type_conversion_numeric"
test_type_conversion_numeric
------------------------------
-1234567890.0987654321
(1 row)
SELECT * FROM test_type_conversion_numeric(null); SELECT * FROM test_type_conversion_numeric(null);
INFO: (None, <class 'NoneType'>) INFO: (None, 'NoneType')
CONTEXT: PL/Python function "test_type_conversion_numeric" CONTEXT: PL/Python function "test_type_conversion_numeric"
test_type_conversion_numeric test_type_conversion_numeric
------------------------------ ------------------------------
......
...@@ -16,6 +16,7 @@ ...@@ -16,6 +16,7 @@
#include "utils/builtins.h" #include "utils/builtins.h"
#include "utils/lsyscache.h" #include "utils/lsyscache.h"
#include "utils/memutils.h" #include "utils/memutils.h"
#include "utils/numeric.h"
#include "utils/syscache.h" #include "utils/syscache.h"
#include "utils/typcache.h" #include "utils/typcache.h"
...@@ -35,7 +36,7 @@ static void PLy_output_datum_func2(PLyObToDatum *arg, HeapTuple typeTup); ...@@ -35,7 +36,7 @@ static void PLy_output_datum_func2(PLyObToDatum *arg, HeapTuple typeTup);
static PyObject *PLyBool_FromBool(PLyDatumToOb *arg, Datum d); static PyObject *PLyBool_FromBool(PLyDatumToOb *arg, Datum d);
static PyObject *PLyFloat_FromFloat4(PLyDatumToOb *arg, Datum d); static PyObject *PLyFloat_FromFloat4(PLyDatumToOb *arg, Datum d);
static PyObject *PLyFloat_FromFloat8(PLyDatumToOb *arg, Datum d); static PyObject *PLyFloat_FromFloat8(PLyDatumToOb *arg, Datum d);
static PyObject *PLyFloat_FromNumeric(PLyDatumToOb *arg, Datum d); static PyObject *PLyDecimal_FromNumeric(PLyDatumToOb *arg, Datum d);
static PyObject *PLyInt_FromInt16(PLyDatumToOb *arg, Datum d); static PyObject *PLyInt_FromInt16(PLyDatumToOb *arg, Datum d);
static PyObject *PLyInt_FromInt32(PLyDatumToOb *arg, Datum d); static PyObject *PLyInt_FromInt32(PLyDatumToOb *arg, Datum d);
static PyObject *PLyLong_FromInt64(PLyDatumToOb *arg, Datum d); static PyObject *PLyLong_FromInt64(PLyDatumToOb *arg, Datum d);
...@@ -450,7 +451,7 @@ PLy_input_datum_func2(PLyDatumToOb *arg, Oid typeOid, HeapTuple typeTup) ...@@ -450,7 +451,7 @@ PLy_input_datum_func2(PLyDatumToOb *arg, Oid typeOid, HeapTuple typeTup)
arg->func = PLyFloat_FromFloat8; arg->func = PLyFloat_FromFloat8;
break; break;
case NUMERICOID: case NUMERICOID:
arg->func = PLyFloat_FromNumeric; arg->func = PLyDecimal_FromNumeric;
break; break;
case INT2OID: case INT2OID:
arg->func = PLyInt_FromInt16; arg->func = PLyInt_FromInt16;
...@@ -516,16 +517,37 @@ PLyFloat_FromFloat8(PLyDatumToOb *arg, Datum d) ...@@ -516,16 +517,37 @@ PLyFloat_FromFloat8(PLyDatumToOb *arg, Datum d)
} }
static PyObject * static PyObject *
PLyFloat_FromNumeric(PLyDatumToOb *arg, Datum d) PLyDecimal_FromNumeric(PLyDatumToOb *arg, Datum d)
{ {
/* static PyObject *decimal_constructor;
* Numeric is cast to a PyFloat: This results in a loss of precision Would char *str;
* it be better to cast to PyString? PyObject *pyvalue;
*/
Datum f = DirectFunctionCall1(numeric_float8, d); /* Try to import cdecimal. If it doesn't exist, fall back to decimal. */
double x = DatumGetFloat8(f); if (!decimal_constructor)
{
PyObject *decimal_module;
decimal_module = PyImport_ImportModule("cdecimal");
if (!decimal_module)
{
PyErr_Clear();
decimal_module = PyImport_ImportModule("decimal");
}
if (!decimal_module)
PLy_elog(ERROR, "could not import a module for Decimal constructor");
decimal_constructor = PyObject_GetAttrString(decimal_module, "Decimal");
if (!decimal_constructor)
PLy_elog(ERROR, "no Decimal attribute in module");
}
str = DatumGetCString(DirectFunctionCall1(numeric_out, d));
pyvalue = PyObject_CallFunction(decimal_constructor, "s", str);
if (!pyvalue)
PLy_elog(ERROR, "conversion from numeric to Decimal failed");
return PyFloat_FromDouble(x); return pyvalue;
} }
static PyObject * static PyObject *
......
...@@ -86,14 +86,19 @@ SELECT * FROM test_type_conversion_int8(null); ...@@ -86,14 +86,19 @@ SELECT * FROM test_type_conversion_int8(null);
CREATE FUNCTION test_type_conversion_numeric(x numeric) RETURNS numeric AS $$ CREATE FUNCTION test_type_conversion_numeric(x numeric) RETURNS numeric AS $$
plpy.info(x, type(x)) # print just the class name, not the type, to avoid differences
# between decimal and cdecimal
plpy.info(x, x.__class__.__name__)
return x return x
$$ LANGUAGE plpythonu; $$ LANGUAGE plpythonu;
/* The current implementation converts numeric to float. */
SELECT * FROM test_type_conversion_numeric(100); SELECT * FROM test_type_conversion_numeric(100);
SELECT * FROM test_type_conversion_numeric(-100); SELECT * FROM test_type_conversion_numeric(-100);
SELECT * FROM test_type_conversion_numeric(100.0);
SELECT * FROM test_type_conversion_numeric(100.00);
SELECT * FROM test_type_conversion_numeric(5000000000.5); SELECT * FROM test_type_conversion_numeric(5000000000.5);
SELECT * FROM test_type_conversion_numeric(1234567890.0987654321);
SELECT * FROM test_type_conversion_numeric(-1234567890.0987654321);
SELECT * FROM test_type_conversion_numeric(null); SELECT * FROM test_type_conversion_numeric(null);
......
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