Merge remote-tracking branch 'nested-array-nulls'

This commit is contained in:
Daniele Varrazzo 2018-05-20 12:51:13 +01:00
commit 098c00d73e
4 changed files with 106 additions and 41 deletions

1
NEWS
View File

@ -19,6 +19,7 @@ What's new in psycopg 2.7.5
- Allow non-ascii chars in namedtuple fields (regression introduced fixing - Allow non-ascii chars in namedtuple fields (regression introduced fixing
:ticket':`#211`). :ticket':`#211`).
- Fixed adaptation of arrays of arrays of nulls (:ticket:`#325`).
- Fixed building on Solaris 11 and derivatives such as SmartOS and illumos - Fixed building on Solaris 11 and derivatives such as SmartOS and illumos
(:ticket:`#677`). (:ticket:`#677`).
- Maybe fixed building on MSYS2 (as reported in :ticket:`#658`). - Maybe fixed building on MSYS2 (as reported in :ticket:`#658`).

View File

@ -38,13 +38,14 @@ list_quote(listObject *self)
{ {
/* adapt the list by calling adapt() recursively and then wrapping /* adapt the list by calling adapt() recursively and then wrapping
everything into "ARRAY[]" */ everything into "ARRAY[]" */
PyObject *tmp = NULL, *str = NULL, *joined = NULL, *res = NULL; PyObject *res = NULL;
PyObject **qs = NULL;
Py_ssize_t bufsize = 0;
char *buf = NULL, *ptr;
/* list consisting of only NULL don't work with the ARRAY[] construct /* list consisting of only NULL don't work with the ARRAY[] construct
* so we use the {NULL,...} syntax. Note however that list of lists where * so we use the {NULL,...} syntax. The same syntax is also necessary
* some element is a list of only null still fails: for that we should use * to convert array of arrays containing only nulls. */
* the '{...}' syntax uniformly but we cannot do it in the current
* infrastructure. TODO in psycopg3 */
int all_nulls = 1; int all_nulls = 1;
Py_ssize_t i, len; Py_ssize_t i, len;
@ -53,47 +54,95 @@ list_quote(listObject *self)
/* empty arrays are converted to NULLs (still searching for a way to /* empty arrays are converted to NULLs (still searching for a way to
insert an empty array in postgresql */ insert an empty array in postgresql */
if (len == 0) return Bytes_FromString("'{}'"); if (len == 0) {
res = Bytes_FromString("'{}'");
goto exit;
}
tmp = PyTuple_New(len); if (!(qs = PyMem_New(PyObject *, len))) {
PyErr_NoMemory();
goto exit;
}
memset(qs, 0, len * sizeof(PyObject *));
for (i=0; i<len; i++) { for (i = 0; i < len; i++) {
PyObject *quoted;
PyObject *wrapped = PyList_GET_ITEM(self->wrapped, i); PyObject *wrapped = PyList_GET_ITEM(self->wrapped, i);
if (wrapped == Py_None) { if (wrapped == Py_None) {
Py_INCREF(psyco_null); Py_INCREF(psyco_null);
quoted = psyco_null; qs[i] = psyco_null;
} }
else { else {
quoted = microprotocol_getquoted(wrapped, if (!(qs[i] = microprotocol_getquoted(
(connectionObject*)self->connection); wrapped, (connectionObject*)self->connection))) {
if (quoted == NULL) goto error; goto exit;
all_nulls = 0; }
/* Lists of arrays containing only nulls are also not supported
* by the ARRAY construct so we should do some special casing */
if (!PyList_Check(wrapped) || Bytes_AS_STRING(qs[i])[0] == 'A') {
all_nulls = 0;
}
} }
bufsize += Bytes_GET_SIZE(qs[i]) + 1; /* this, and a comma */
/* here we don't loose a refcnt: SET_ITEM does not change the
reference count and we are just transferring ownership of the tmp
object to the tuple */
PyTuple_SET_ITEM(tmp, i, quoted);
} }
/* now that we have a tuple of adapted objects we just need to join them /* Create an array literal, usually ARRAY[...] but if the contents are
and put "ARRAY[] around the result */ * all NULL or array of NULL we must use the '{...}' syntax
str = Bytes_FromString(", "); */
joined = PyObject_CallMethod(str, "join", "(O)", tmp); if (!(ptr = buf = PyMem_Malloc(bufsize + 8))) {
if (joined == NULL) goto error; PyErr_NoMemory();
goto exit;
}
/* PG doesn't like ARRAY[NULL..] */
if (!all_nulls) { if (!all_nulls) {
res = Bytes_FromFormat("ARRAY[%s]", Bytes_AsString(joined)); strcpy(ptr, "ARRAY[");
} else { ptr += 6;
res = Bytes_FromFormat("'{%s}'", Bytes_AsString(joined)); for (i = 0; i < len; i++) {
Py_ssize_t sl;
sl = Bytes_GET_SIZE(qs[i]);
memcpy(ptr, Bytes_AS_STRING(qs[i]), sl);
ptr += sl;
*ptr++ = ',';
}
*(ptr - 1) = ']';
}
else {
*ptr++ = '\'';
*ptr++ = '{';
for (i = 0; i < len; i++) {
/* in case all the adapted things are nulls (or array of nulls),
* the quoted string is either NULL or an array of the form
* '{NULL,...}', in which case we have to strip the extra quotes */
char *s;
Py_ssize_t sl;
s = Bytes_AS_STRING(qs[i]);
sl = Bytes_GET_SIZE(qs[i]);
if (s[0] != '\'') {
memcpy(ptr, s, sl);
ptr += sl;
}
else {
memcpy(ptr, s + 1, sl - 2);
ptr += sl - 2;
}
*ptr++ = ',';
}
*(ptr - 1) = '}';
*ptr++ = '\'';
} }
error: res = Bytes_FromStringAndSize(buf, ptr - buf);
Py_XDECREF(tmp);
Py_XDECREF(str); exit:
Py_XDECREF(joined); if (qs) {
for (i = 0; i < len; i++) {
PyObject *q = qs[i];
Py_XDECREF(q);
}
PyMem_Free(qs);
}
PyMem_Free(buf);
return res; return res;
} }

View File

@ -224,16 +224,31 @@ class TypesBasicTests(ConnectingTestCase):
curs.execute("insert into na (boola) values (%s)", ([True, None],)) curs.execute("insert into na (boola) values (%s)", ([True, None],))
curs.execute("insert into na (boola) values (%s)", ([None, None],)) curs.execute("insert into na (boola) values (%s)", ([None, None],))
# TODO: array of array of nulls are not supported yet curs.execute("insert into na (textaa) values (%s)", ([[None]],))
# curs.execute("insert into na (textaa) values (%s)", ([[None]],))
curs.execute("insert into na (textaa) values (%s)", ([['a', None]],)) curs.execute("insert into na (textaa) values (%s)", ([['a', None]],))
# curs.execute("insert into na (textaa) values (%s)", ([[None, None]],)) curs.execute("insert into na (textaa) values (%s)", ([[None, None]],))
# curs.execute("insert into na (intaa) values (%s)", ([[None]],))
curs.execute("insert into na (intaa) values (%s)", ([[None]],))
curs.execute("insert into na (intaa) values (%s)", ([[42, None]],)) curs.execute("insert into na (intaa) values (%s)", ([[42, None]],))
# curs.execute("insert into na (intaa) values (%s)", ([[None, None]],)) curs.execute("insert into na (intaa) values (%s)", ([[None, None]],))
# curs.execute("insert into na (boolaa) values (%s)", ([[None]],))
curs.execute("insert into na (boolaa) values (%s)", ([[None]],))
curs.execute("insert into na (boolaa) values (%s)", ([[True, None]],)) curs.execute("insert into na (boolaa) values (%s)", ([[True, None]],))
# curs.execute("insert into na (boolaa) values (%s)", ([[None, None]],)) curs.execute("insert into na (boolaa) values (%s)", ([[None, None]],))
@testutils.skip_before_postgres(8, 2)
def testNestedArrays(self):
curs = self.conn.cursor()
for a in [
[[1]],
[[None]],
[[None, None, None]],
[[None, None], [1, None]],
[[None, None], [None, None]],
[[[None, None], [None, None]]],
]:
curs.execute("select %s::int[]", (a,))
self.assertEqual(curs.fetchone()[0], a)
@testutils.skip_from_python(3) @testutils.skip_from_python(3)
def testTypeRoundtripBuffer(self): def testTypeRoundtripBuffer(self):

View File

@ -179,8 +179,8 @@ class HstoreTestCase(ConnectingTestCase):
m = re.match(br'hstore\(ARRAY\[([^\]]+)\], ARRAY\[([^\]]+)\]\)', q) m = re.match(br'hstore\(ARRAY\[([^\]]+)\], ARRAY\[([^\]]+)\]\)', q)
self.assert_(m, repr(q)) self.assert_(m, repr(q))
kk = m.group(1).split(b", ") kk = m.group(1).split(b",")
vv = m.group(2).split(b", ") vv = m.group(2).split(b",")
ii = list(zip(kk, vv)) ii = list(zip(kk, vv))
ii.sort() ii.sort()