diff options
author | Peter Eisentraut <peter_e@gmx.net> | 2013-07-05 22:41:25 -0400 |
---|---|---|
committer | Peter Eisentraut <peter_e@gmx.net> | 2013-07-05 22:41:25 -0400 |
commit | 7919398bac8bacd75ec5d763ce8b15ffaaa3e071 (patch) | |
tree | e5c1a77a161560da6ce1a6879f9bf3d3408a81eb | |
parent | 02d2b694ee42a9e241d37ce67df122fff43d5bb9 (diff) | |
download | postgresql-7919398bac8bacd75ec5d763ce8b15ffaaa3e071.tar.gz |
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: Steve Singer <steve@ssinger.info>
-rw-r--r-- | doc/src/sgml/plpython.sgml | 23 | ||||
-rw-r--r-- | src/pl/plpython/expected/plpython_types.out | 49 | ||||
-rw-r--r-- | src/pl/plpython/expected/plpython_types_3.out | 49 | ||||
-rw-r--r-- | src/pl/plpython/plpy_typeio.c | 42 | ||||
-rw-r--r-- | src/pl/plpython/sql/plpython_types.sql | 9 |
5 files changed, 138 insertions, 34 deletions
diff --git a/doc/src/sgml/plpython.sgml b/doc/src/sgml/plpython.sgml index aaf758d495..ad89355d60 100644 --- a/doc/src/sgml/plpython.sgml +++ b/doc/src/sgml/plpython.sgml @@ -310,12 +310,23 @@ $$ LANGUAGE plpythonu; <listitem> <para> - PostgreSQL <type>real</type>, <type>double</type>, - and <type>numeric</type> are converted to - Python <type>float</type>. Note that for - the <type>numeric</type> this loses information and can lead to - incorrect results. This might be fixed in a future - release. + PostgreSQL <type>real</type> and <type>double</type> are converted to + Python <type>float</type>. + </para> + </listitem> + + <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> </listitem> diff --git a/src/pl/plpython/expected/plpython_types.out b/src/pl/plpython/expected/plpython_types.out index 46413455c8..edc51423e9 100644 --- a/src/pl/plpython/expected/plpython_types.out +++ b/src/pl/plpython/expected/plpython_types.out @@ -213,36 +213,69 @@ CONTEXT: PL/Python function "test_type_conversion_int8" (1 row) 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 $$ LANGUAGE plpythonu; -/* The current implementation converts numeric to float. */ SELECT * FROM test_type_conversion_numeric(100); -INFO: (100.0, <type 'float'>) +INFO: (Decimal('100'), 'Decimal') CONTEXT: PL/Python function "test_type_conversion_numeric" test_type_conversion_numeric ------------------------------ - 100.0 + 100 (1 row) SELECT * FROM test_type_conversion_numeric(-100); -INFO: (-100.0, <type 'float'>) +INFO: (Decimal('-100'), 'Decimal') +CONTEXT: PL/Python function "test_type_conversion_numeric" + test_type_conversion_numeric +------------------------------ + -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 + 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) 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" test_type_conversion_numeric ------------------------------ 5000000000.5 (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); -INFO: (None, <type 'NoneType'>) +INFO: (None, 'NoneType') CONTEXT: PL/Python function "test_type_conversion_numeric" test_type_conversion_numeric ------------------------------ diff --git a/src/pl/plpython/expected/plpython_types_3.out b/src/pl/plpython/expected/plpython_types_3.out index 511ef5a4c9..11c4c478c4 100644 --- a/src/pl/plpython/expected/plpython_types_3.out +++ b/src/pl/plpython/expected/plpython_types_3.out @@ -213,36 +213,69 @@ CONTEXT: PL/Python function "test_type_conversion_int8" (1 row) 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 $$ LANGUAGE plpython3u; -/* The current implementation converts numeric to float. */ SELECT * FROM test_type_conversion_numeric(100); -INFO: (100.0, <class 'float'>) +INFO: (Decimal('100'), 'Decimal') CONTEXT: PL/Python function "test_type_conversion_numeric" test_type_conversion_numeric ------------------------------ - 100.0 + 100 (1 row) SELECT * FROM test_type_conversion_numeric(-100); -INFO: (-100.0, <class 'float'>) +INFO: (Decimal('-100'), 'Decimal') +CONTEXT: PL/Python function "test_type_conversion_numeric" + test_type_conversion_numeric +------------------------------ + -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 + 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) 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" test_type_conversion_numeric ------------------------------ 5000000000.5 (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); -INFO: (None, <class 'NoneType'>) +INFO: (None, 'NoneType') CONTEXT: PL/Python function "test_type_conversion_numeric" test_type_conversion_numeric ------------------------------ diff --git a/src/pl/plpython/plpy_typeio.c b/src/pl/plpython/plpy_typeio.c index 6a9a2cb974..caccbf9b88 100644 --- a/src/pl/plpython/plpy_typeio.c +++ b/src/pl/plpython/plpy_typeio.c @@ -16,6 +16,7 @@ #include "utils/builtins.h" #include "utils/lsyscache.h" #include "utils/memutils.h" +#include "utils/numeric.h" #include "utils/syscache.h" #include "utils/typcache.h" @@ -35,7 +36,7 @@ static void PLy_output_datum_func2(PLyObToDatum *arg, HeapTuple typeTup); static PyObject *PLyBool_FromBool(PLyDatumToOb *arg, Datum d); static PyObject *PLyFloat_FromFloat4(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_FromInt32(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) arg->func = PLyFloat_FromFloat8; break; case NUMERICOID: - arg->func = PLyFloat_FromNumeric; + arg->func = PLyDecimal_FromNumeric; break; case INT2OID: arg->func = PLyInt_FromInt16; @@ -516,16 +517,37 @@ PLyFloat_FromFloat8(PLyDatumToOb *arg, Datum d) } static PyObject * -PLyFloat_FromNumeric(PLyDatumToOb *arg, Datum d) +PLyDecimal_FromNumeric(PLyDatumToOb *arg, Datum d) { - /* - * Numeric is cast to a PyFloat: This results in a loss of precision Would - * it be better to cast to PyString? - */ - Datum f = DirectFunctionCall1(numeric_float8, d); - double x = DatumGetFloat8(f); + static PyObject *decimal_constructor; + char *str; + PyObject *pyvalue; + + /* Try to import cdecimal. If it doesn't exist, fall back to decimal. */ + 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 * diff --git a/src/pl/plpython/sql/plpython_types.sql b/src/pl/plpython/sql/plpython_types.sql index 6a50b4236d..6881880729 100644 --- a/src/pl/plpython/sql/plpython_types.sql +++ b/src/pl/plpython/sql/plpython_types.sql @@ -86,14 +86,19 @@ SELECT * FROM test_type_conversion_int8(null); 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 $$ 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.0); +SELECT * FROM test_type_conversion_numeric(100.00); 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); |