]> granicus.if.org Git - postgresql/commitdiff
PL/Python: Convert numeric to Decimal
authorPeter Eisentraut <peter_e@gmx.net>
Sat, 6 Jul 2013 02:41:25 +0000 (22:41 -0400)
committerPeter Eisentraut <peter_e@gmx.net>
Sat, 6 Jul 2013 02:41:25 +0000 (22:41 -0400)
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>
doc/src/sgml/plpython.sgml
src/pl/plpython/expected/plpython_types.out
src/pl/plpython/expected/plpython_types_3.out
src/pl/plpython/plpy_typeio.c
src/pl/plpython/sql/plpython_types.sql

index aaf758d49596728c7724d0f47eef6bd62e57bb2f..ad89355d60d5741e7ab9a9d456bee24b9a741eae 100644 (file)
@@ -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>
 
index 46413455c8be3e226cc431564c607a455b8ec9af..edc51423e96bd04696d0cb0d11753298e865b381 100644 (file)
@@ -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 
 ------------------------------
index 511ef5a4c9a976bddd90b5e572eb81ddbf9cf629..11c4c478c4fbe3bb90cdc74e1b1c78f5ea1c3587 100644 (file)
@@ -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 
 ------------------------------
index 6a9a2cb974721d8ee37165222402d1679110fbe5..caccbf9b88ff6e373e5f1a2858e2a56ae4f60275 100644 (file)
@@ -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 *
index 6a50b4236db0fbc8d2cbdb1ea3f7a9fb8005ddea..68818807299b0074850cee7cc05565f7e1d8b1ad 100644 (file)
@@ -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);