From 6f7b6d373f84dbf176cf42444b113b4e4763e444 Mon Sep 17 00:00:00 2001
From: Benjamin Poulain <ikipou@gmail.com>
Date: Sun, 20 Feb 2011 02:34:54 +0100
Subject: [PATCH] Add a type converter to handle untyped empty arrays.

Empty array can be returned untyped by postgres. To handle
this case, a special handler is added for the type UNKNOWNOID.
If the value return by the database is strictly equal to "{}",
the value is converted. Otherwise, the conversion fallback on
the default handler.
---
 psycopg/adapter_list.c      |    2 +-
 psycopg/typecast.c          |   26 ++++++++++++++++++++++----
 psycopg/typecast_builtins.c |    2 ++
 tests/types_basic.py        |   12 ++++++++++--
 4 files changed, 35 insertions(+), 7 deletions(-)

diff --git a/psycopg/adapter_list.c b/psycopg/adapter_list.c
index b7e1f96..7bfc1c8 100644
--- a/psycopg/adapter_list.c
+++ b/psycopg/adapter_list.c
@@ -45,7 +45,7 @@ list_quote(listObject *self)
 
     /* empty arrays are converted to NULLs (still searching for a way to
        insert an empty array in postgresql */
-    if (len == 0) return Bytes_FromString("'{}'::text[]");
+    if (len == 0) return Bytes_FromString("'{}'");
 
     tmp = PyTuple_New(len);
 
diff --git a/psycopg/typecast.c b/psycopg/typecast.c
index 484ce45..bcd9392 100644
--- a/psycopg/typecast.c
+++ b/psycopg/typecast.c
@@ -177,6 +177,28 @@ typecast_parse_time(const char* s, const char** t, Py_ssize_t* len,
 #endif
 
 #include "psycopg/typecast_array.c"
+
+static long int typecast_default_DEFAULT[] = {0};
+static typecastObject_initlist typecast_default = {
+    "DEFAULT", typecast_default_DEFAULT, typecast_STRING_cast};
+
+static PyObject *
+typecast_UNKNOWN_cast(const char *str, Py_ssize_t len, PyObject *curs)
+{
+    Dprintf("typecast_UNKNOWN_cast: str = '%s',"
+            " len = " FORMAT_CODE_PY_SSIZE_T, str, len);
+    
+    // PostgreSQL returns {} for empty array without explicit type. We convert
+    // that to list in order to handle empty lists.
+    if (len == 2 && str[0] == '{' && str[1] == '}') {
+        return PyList_New(0);
+    }
+    
+    Dprintf("typecast_UNKNOWN_cast: fallback to default cast");
+    
+    return typecast_default.cast(str, len, curs);
+}
+
 #include "psycopg/typecast_builtins.c"
 
 #define typecast_PYDATETIMEARRAY_cast typecast_GENERIC_ARRAY_cast
@@ -225,10 +247,6 @@ PyObject *psyco_default_cast;
 PyObject *psyco_binary_types;
 PyObject *psyco_default_binary_cast;
 
-static long int typecast_default_DEFAULT[] = {0};
-static typecastObject_initlist typecast_default = {
-    "DEFAULT", typecast_default_DEFAULT, typecast_STRING_cast};
-
 
 /* typecast_init - initialize the dictionary and create default types */
 
diff --git a/psycopg/typecast_builtins.c b/psycopg/typecast_builtins.c
index e8e5a1a..a104b7c 100644
--- a/psycopg/typecast_builtins.c
+++ b/psycopg/typecast_builtins.c
@@ -25,6 +25,7 @@ static long int typecast_DATEARRAY_types[] = {1182, 0};
 static long int typecast_INTERVALARRAY_types[] = {1187, 0};
 static long int typecast_BINARYARRAY_types[] = {1001, 0};
 static long int typecast_ROWIDARRAY_types[] = {1028, 1013, 0};
+static long int typecast_UNKNOWN_types[] = {705, 0};
 
 
 static typecastObject_initlist typecast_builtins[] = {
@@ -55,6 +56,7 @@ static typecastObject_initlist typecast_builtins[] = {
   {"INTERVALARRAY", typecast_INTERVALARRAY_types, typecast_INTERVALARRAY_cast, "INTERVAL"},
   {"BINARYARRAY", typecast_BINARYARRAY_types, typecast_BINARYARRAY_cast, "BINARY"},
   {"ROWIDARRAY", typecast_ROWIDARRAY_types, typecast_ROWIDARRAY_cast, "ROWID"},
+  {"UNKNOWN", typecast_UNKNOWN_types, typecast_UNKNOWN_cast, NULL},
     {NULL, NULL, NULL, NULL}
 };
 
diff --git a/tests/types_basic.py b/tests/types_basic.py
index 43bae93..7a1b778 100755
--- a/tests/types_basic.py
+++ b/tests/types_basic.py
@@ -152,13 +152,21 @@ class TypesBasicTests(unittest.TestCase):
             self.assertEqual(s, buf2)
 
     def testArray(self):
-        s = self.execute("SELECT %s AS foo", ([],))
-        self.failUnlessEqual(s, [])
         s = self.execute("SELECT %s AS foo", ([[1,2],[3,4]],))
         self.failUnlessEqual(s, [[1,2],[3,4]])
         s = self.execute("SELECT %s AS foo", (['one', 'two', 'three'],))
         self.failUnlessEqual(s, ['one', 'two', 'three'])
 
+    def testEmptyArray(self):
+        s = self.execute("SELECT '{}' AS foo")
+        self.failUnlessEqual(s, [])
+        s = self.execute("SELECT '{}'::text[] AS foo")
+        self.failUnlessEqual(s, [])
+        s = self.execute("SELECT %s AS foo", ([],))
+        self.failUnlessEqual(s, [])
+        s = self.execute("SELECT 1 != ALL(%s)", ([],))
+        self.failUnlessEqual(s, True)
+
     @testutils.skip_on_python3
     def testTypeRoundtripBuffer(self):
         o1 = buffer("".join(map(chr, range(256))))
-- 
1.7.3.5

