Skip to content

Commit 510e1b8

Browse files
committed
Give a hint, when [] is incorrectly used for a composite type in array.
That used to be accepted, so let's try to give a hint to users on why their PL/python functions no longer work. Reviewed by Pavel Stehule. Discussion: <CAH38_tmbqwaUyKs9yagyRra=SMaT45FPBxk1pmTYcM0TyXGG7Q@mail.gmail.com>
1 parent 94aceed commit 510e1b8

File tree

7 files changed

+129
-39
lines changed

7 files changed

+129
-39
lines changed

src/pl/plpython/expected/plpython_composite.out

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -579,3 +579,16 @@ SELECT * FROM composite_type_as_list();
579579
{{"(first,1)","(second,1)"},{"(first,2)","(second,2)"},{"(first,3)","(second,3)"}}
580580
(1 row)
581581

582+
-- Starting with PostgreSQL 10, a composite type in an array cannot be
583+
-- represented as a Python list, because it's ambiguous with multi-dimensional
584+
-- arrays. So this throws an error now. The error should contain a useful hint
585+
-- on the issue.
586+
CREATE FUNCTION composite_type_as_list_broken() RETURNS type_record[] AS $$
587+
return [['first', 1]];
588+
$$ LANGUAGE plpythonu;
589+
SELECT * FROM composite_type_as_list_broken();
590+
ERROR: malformed record literal: "first"
591+
DETAIL: Missing left parenthesis.
592+
HINT: To return a composite type in an array, return the composite type as a Python tuple, e.g. "[('foo')]"
593+
CONTEXT: while creating return value
594+
PL/Python function "composite_type_as_list_broken"

src/pl/plpython/plpy_cursorobject.c

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -240,7 +240,8 @@ PLy_cursor_plan(PyObject *ob, PyObject *args)
240240
plan->values[j] =
241241
plan->args[j].out.d.func(&(plan->args[j].out.d),
242242
-1,
243-
elem);
243+
elem,
244+
false);
244245
}
245246
PG_CATCH();
246247
{

src/pl/plpython/plpy_exec.c

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -245,15 +245,15 @@ PLy_exec_function(FunctionCallInfo fcinfo, PLyProcedure *proc)
245245
desc = lookup_rowtype_tupdesc(proc->result.out.d.typoid,
246246
proc->result.out.d.typmod);
247247

248-
rv = PLyObject_ToCompositeDatum(&proc->result, desc, plrv);
248+
rv = PLyObject_ToCompositeDatum(&proc->result, desc, plrv, false);
249249
fcinfo->isnull = (rv == (Datum) NULL);
250250

251251
ReleaseTupleDesc(desc);
252252
}
253253
else
254254
{
255255
fcinfo->isnull = false;
256-
rv = (proc->result.out.d.func) (&proc->result.out.d, -1, plrv);
256+
rv = (proc->result.out.d.func) (&proc->result.out.d, -1, plrv, false);
257257
}
258258
}
259259
PG_CATCH();
@@ -984,7 +984,8 @@ PLy_modify_tuple(PLyProcedure *proc, PyObject *pltd, TriggerData *tdata,
984984

985985
modvalues[i] = (att->func) (att,
986986
tupdesc->attrs[atti]->atttypmod,
987-
plval);
987+
plval,
988+
false);
988989
modnulls[i] = ' ';
989990
}
990991
else

src/pl/plpython/plpy_spi.c

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -264,7 +264,8 @@ PLy_spi_execute_plan(PyObject *ob, PyObject *list, long limit)
264264
plan->values[j] =
265265
plan->args[j].out.d.func(&(plan->args[j].out.d),
266266
-1,
267-
elem);
267+
elem,
268+
false);
268269
}
269270
PG_CATCH();
270271
{

src/pl/plpython/plpy_typeio.c

Lines changed: 87 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
#include "parser/parse_type.h"
1515
#include "utils/array.h"
1616
#include "utils/builtins.h"
17+
#include "utils/fmgroids.h"
1718
#include "utils/lsyscache.h"
1819
#include "utils/memutils.h"
1920
#include "utils/numeric.h"
@@ -49,21 +50,21 @@ static PyObject *PLyList_FromArray_recurse(PLyDatumToOb *elm, int *dims, int ndi
4950
char **dataptr_p, bits8 **bitmap_p, int *bitmask_p);
5051

5152
/* conversion from Python objects to Datums */
52-
static Datum PLyObject_ToBool(PLyObToDatum *arg, int32 typmod, PyObject *plrv);
53-
static Datum PLyObject_ToBytea(PLyObToDatum *arg, int32 typmod, PyObject *plrv);
54-
static Datum PLyObject_ToComposite(PLyObToDatum *arg, int32 typmod, PyObject *plrv);
55-
static Datum PLyObject_ToDatum(PLyObToDatum *arg, int32 typmod, PyObject *plrv);
56-
static Datum PLyObject_ToTransform(PLyObToDatum *arg, int32 typmod, PyObject *plrv);
57-
static Datum PLySequence_ToArray(PLyObToDatum *arg, int32 typmod, PyObject *plrv);
53+
static Datum PLyObject_ToBool(PLyObToDatum *arg, int32 typmod, PyObject *plrv, bool inarray);
54+
static Datum PLyObject_ToBytea(PLyObToDatum *arg, int32 typmod, PyObject *plrv, bool inarray);
55+
static Datum PLyObject_ToComposite(PLyObToDatum *arg, int32 typmod, PyObject *plrv, bool inarray);
56+
static Datum PLyObject_ToDatum(PLyObToDatum *arg, int32 typmod, PyObject *plrv, bool inarray);
57+
static Datum PLyObject_ToTransform(PLyObToDatum *arg, int32 typmod, PyObject *plrv, bool inarray);
58+
static Datum PLySequence_ToArray(PLyObToDatum *arg, int32 typmod, PyObject *plrv, bool inarray);
5859
static void PLySequence_ToArray_recurse(PLyObToDatum *elm, PyObject *list,
5960
int *dims, int ndim, int dim,
6061
Datum *elems, bool *nulls, int *currelem);
6162

6263
/* conversion from Python objects to composite Datums (used by triggers and SRFs) */
63-
static Datum PLyString_ToComposite(PLyTypeInfo *info, TupleDesc desc, PyObject *string);
64+
static Datum PLyString_ToComposite(PLyTypeInfo *info, TupleDesc desc, PyObject *string, bool inarray);
6465
static Datum PLyMapping_ToComposite(PLyTypeInfo *info, TupleDesc desc, PyObject *mapping);
6566
static Datum PLySequence_ToComposite(PLyTypeInfo *info, TupleDesc desc, PyObject *sequence);
66-
static Datum PLyGenericObject_ToComposite(PLyTypeInfo *info, TupleDesc desc, PyObject *object);
67+
static Datum PLyGenericObject_ToComposite(PLyTypeInfo *info, TupleDesc desc, PyObject *object, bool inarray);
6768

6869
void
6970
PLy_typeinfo_init(PLyTypeInfo *arg, MemoryContext mcxt)
@@ -341,12 +342,12 @@ PLyDict_FromTuple(PLyTypeInfo *info, HeapTuple tuple, TupleDesc desc)
341342
* as an object that has __getattr__ support.
342343
*/
343344
Datum
344-
PLyObject_ToCompositeDatum(PLyTypeInfo *info, TupleDesc desc, PyObject *plrv)
345+
PLyObject_ToCompositeDatum(PLyTypeInfo *info, TupleDesc desc, PyObject *plrv, bool inarray)
345346
{
346347
Datum datum;
347348

348349
if (PyString_Check(plrv) || PyUnicode_Check(plrv))
349-
datum = PLyString_ToComposite(info, desc, plrv);
350+
datum = PLyString_ToComposite(info, desc, plrv, inarray);
350351
else if (PySequence_Check(plrv))
351352
/* composite type as sequence (tuple, list etc) */
352353
datum = PLySequence_ToComposite(info, desc, plrv);
@@ -355,7 +356,7 @@ PLyObject_ToCompositeDatum(PLyTypeInfo *info, TupleDesc desc, PyObject *plrv)
355356
datum = PLyMapping_ToComposite(info, desc, plrv);
356357
else
357358
/* returned as smth, must provide method __getattr__(name) */
358-
datum = PLyGenericObject_ToComposite(info, desc, plrv);
359+
datum = PLyGenericObject_ToComposite(info, desc, plrv, inarray);
359360

360361
return datum;
361362
}
@@ -746,7 +747,7 @@ PLyList_FromArray_recurse(PLyDatumToOb *elm, int *dims, int ndim, int dim,
746747
* type can parse.
747748
*/
748749
static Datum
749-
PLyObject_ToBool(PLyObToDatum *arg, int32 typmod, PyObject *plrv)
750+
PLyObject_ToBool(PLyObToDatum *arg, int32 typmod, PyObject *plrv, bool inarray)
750751
{
751752
Datum rv;
752753

@@ -765,7 +766,7 @@ PLyObject_ToBool(PLyObToDatum *arg, int32 typmod, PyObject *plrv)
765766
* with embedded nulls. And it's faster this way.
766767
*/
767768
static Datum
768-
PLyObject_ToBytea(PLyObToDatum *arg, int32 typmod, PyObject *plrv)
769+
PLyObject_ToBytea(PLyObToDatum *arg, int32 typmod, PyObject *plrv, bool inarray)
769770
{
770771
PyObject *volatile plrv_so = NULL;
771772
Datum rv;
@@ -809,7 +810,7 @@ PLyObject_ToBytea(PLyObToDatum *arg, int32 typmod, PyObject *plrv)
809810
* for obtaining PostgreSQL tuples.
810811
*/
811812
static Datum
812-
PLyObject_ToComposite(PLyObToDatum *arg, int32 typmod, PyObject *plrv)
813+
PLyObject_ToComposite(PLyObToDatum *arg, int32 typmod, PyObject *plrv, bool inarray)
813814
{
814815
Datum rv;
815816
PLyTypeInfo info;
@@ -836,7 +837,7 @@ PLyObject_ToComposite(PLyObToDatum *arg, int32 typmod, PyObject *plrv)
836837
* that info instead of looking it up every time a tuple is returned from
837838
* the function.
838839
*/
839-
rv = PLyObject_ToCompositeDatum(&info, desc, plrv);
840+
rv = PLyObject_ToCompositeDatum(&info, desc, plrv, inarray);
840841

841842
ReleaseTupleDesc(desc);
842843

@@ -908,26 +909,70 @@ PLyObject_AsString(PyObject *plrv)
908909
* cstring into PostgreSQL type.
909910
*/
910911
static Datum
911-
PLyObject_ToDatum(PLyObToDatum *arg, int32 typmod, PyObject *plrv)
912+
PLyObject_ToDatum(PLyObToDatum *arg, int32 typmod, PyObject *plrv, bool inarray)
912913
{
914+
char *str;
915+
913916
Assert(plrv != Py_None);
914917

918+
str = PLyObject_AsString(plrv);
919+
920+
/*
921+
* If we are parsing a composite type within an array, and the string
922+
* isn't a valid record literal, there's a high chance that the function
923+
* did something like:
924+
*
925+
* CREATE FUNCTION .. RETURNS comptype[] AS $$ return [['foo', 'bar']] $$
926+
* LANGUAGE plpython;
927+
*
928+
* Before PostgreSQL 10, that was interpreted as a single-dimensional
929+
* array, containing record ('foo', 'bar'). PostgreSQL 10 added support
930+
* for multi-dimensional arrays, and it is now interpreted as a
931+
* two-dimensional array, containing two records, 'foo', and 'bar'.
932+
* record_in() will throw an error, because "foo" is not a valid record
933+
* literal.
934+
*
935+
* To make that less confusing to users who are upgrading from older
936+
* versions, try to give a hint in the typical instances of that. If we are
937+
* parsing an array of composite types, and we see a string literal that
938+
* is not a valid record literal, give a hint. We only want to give the
939+
* hint in the narrow case of a malformed string literal, not any error
940+
* from record_in(), so check for that case here specifically.
941+
*
942+
* This check better match the one in record_in(), so that we don't forbid
943+
* literals that are actually valid!
944+
*/
945+
if (inarray && arg->typfunc.fn_oid == F_RECORD_IN)
946+
{
947+
char *ptr = str;
948+
949+
/* Allow leading whitespace */
950+
while (*ptr && isspace((unsigned char) *ptr))
951+
ptr++;
952+
if (*ptr++ != '(')
953+
ereport(ERROR,
954+
(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
955+
errmsg("malformed record literal: \"%s\"", str),
956+
errdetail("Missing left parenthesis."),
957+
errhint("To return a composite type in an array, return the composite type as a Python tuple, e.g. \"[('foo')]\"")));
958+
}
959+
915960
return InputFunctionCall(&arg->typfunc,
916-
PLyObject_AsString(plrv),
961+
str,
917962
arg->typioparam,
918963
typmod);
919964
}
920965

921966

922967
static Datum
923-
PLyObject_ToTransform(PLyObToDatum *arg, int32 typmod, PyObject *plrv)
968+
PLyObject_ToTransform(PLyObToDatum *arg, int32 typmod, PyObject *plrv, bool inarray)
924969
{
925970
return FunctionCall1(&arg->typtransform, PointerGetDatum(plrv));
926971
}
927972

928973

929974
static Datum
930-
PLySequence_ToArray(PLyObToDatum *arg, int32 typmod, PyObject *plrv)
975+
PLySequence_ToArray(PLyObToDatum *arg, int32 typmod, PyObject *plrv, bool inarray)
931976
{
932977
ArrayType *array;
933978
int i;
@@ -1085,7 +1130,7 @@ PLySequence_ToArray_recurse(PLyObToDatum *elm, PyObject *list,
10851130
else
10861131
{
10871132
nulls[*currelem] = false;
1088-
elems[*currelem] = elm->func(elm, -1, obj);
1133+
elems[*currelem] = elm->func(elm, -1, obj, true);
10891134
}
10901135
Py_XDECREF(obj);
10911136
(*currelem)++;
@@ -1095,7 +1140,7 @@ PLySequence_ToArray_recurse(PLyObToDatum *elm, PyObject *list,
10951140

10961141

10971142
static Datum
1098-
PLyString_ToComposite(PLyTypeInfo *info, TupleDesc desc, PyObject *string)
1143+
PLyString_ToComposite(PLyTypeInfo *info, TupleDesc desc, PyObject *string, bool inarray)
10991144
{
11001145
Datum result;
11011146
HeapTuple typeTup;
@@ -1120,7 +1165,7 @@ PLyString_ToComposite(PLyTypeInfo *info, TupleDesc desc, PyObject *string)
11201165

11211166
ReleaseSysCache(typeTup);
11221167

1123-
result = PLyObject_ToDatum(&locinfo.out.d, desc->tdtypmod, string);
1168+
result = PLyObject_ToDatum(&locinfo.out.d, desc->tdtypmod, string, inarray);
11241169

11251170
MemoryContextDelete(cxt);
11261171

@@ -1172,7 +1217,7 @@ PLyMapping_ToComposite(PLyTypeInfo *info, TupleDesc desc, PyObject *mapping)
11721217
}
11731218
else if (value)
11741219
{
1175-
values[i] = (att->func) (att, -1, value);
1220+
values[i] = (att->func) (att, -1, value, false);
11761221
nulls[i] = false;
11771222
}
11781223
else
@@ -1265,7 +1310,7 @@ PLySequence_ToComposite(PLyTypeInfo *info, TupleDesc desc, PyObject *sequence)
12651310
}
12661311
else if (value)
12671312
{
1268-
values[i] = (att->func) (att, -1, value);
1313+
values[i] = (att->func) (att, -1, value, false);
12691314
nulls[i] = false;
12701315
}
12711316

@@ -1294,7 +1339,7 @@ PLySequence_ToComposite(PLyTypeInfo *info, TupleDesc desc, PyObject *sequence)
12941339

12951340

12961341
static Datum
1297-
PLyGenericObject_ToComposite(PLyTypeInfo *info, TupleDesc desc, PyObject *object)
1342+
PLyGenericObject_ToComposite(PLyTypeInfo *info, TupleDesc desc, PyObject *object, bool inarray)
12981343
{
12991344
Datum result;
13001345
HeapTuple tuple;
@@ -1335,16 +1380,29 @@ PLyGenericObject_ToComposite(PLyTypeInfo *info, TupleDesc desc, PyObject *object
13351380
}
13361381
else if (value)
13371382
{
1338-
values[i] = (att->func) (att, -1, value);
1383+
values[i] = (att->func) (att, -1, value, false);
13391384
nulls[i] = false;
13401385
}
13411386
else
1387+
{
1388+
/*
1389+
* No attribute for this column in the object.
1390+
*
1391+
* If we are parsing a composite type in an array, a likely
1392+
* cause is that the function contained something like "[[123,
1393+
* 'foo']]". Before PostgreSQL 10, that was interpreted as an
1394+
* array, with a composite type (123, 'foo') in it. But now
1395+
* it's interpreted as a two-dimensional array, and we try to
1396+
* interpret "123" as the composite type. See also similar
1397+
* heuristic in PLyObject_ToDatum().
1398+
*/
13421399
ereport(ERROR,
13431400
(errcode(ERRCODE_UNDEFINED_COLUMN),
13441401
errmsg("attribute \"%s\" does not exist in Python object", key),
1345-
errhint("To return null in a column, "
1346-
"let the returned object have an attribute named "
1347-
"after column with value None.")));
1402+
inarray ?
1403+
errhint("To return a composite type in an array, return the composite type as a Python tuple, e.g. \"[('foo')]\"") :
1404+
errhint("To return null in a column, let the returned object have an attribute named after column with value None.")));
1405+
}
13481406

13491407
Py_XDECREF(value);
13501408
value = NULL;

src/pl/plpython/plpy_typeio.h

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,11 @@
1010
#include "fmgr.h"
1111
#include "storage/itemptr.h"
1212

13+
/*
14+
* Conversion from PostgreSQL Datum to a Python object.
15+
*/
1316
struct PLyDatumToOb;
14-
typedef PyObject *(*PLyDatumToObFunc) (struct PLyDatumToOb *, Datum);
17+
typedef PyObject *(*PLyDatumToObFunc) (struct PLyDatumToOb *arg, Datum val);
1518

1619
typedef struct PLyDatumToOb
1720
{
@@ -39,11 +42,15 @@ typedef union PLyTypeInput
3942
PLyTupleToOb r;
4043
} PLyTypeInput;
4144

42-
/* convert PyObject to a Postgresql Datum or tuple.
43-
* output from Python
45+
/*
46+
* Conversion from Python object to a Postgresql Datum.
47+
*
48+
* The 'inarray' argument to the conversion function is true, if the
49+
* converted value was in an array (Python list). It is used to give a
50+
* better error message in some cases.
4451
*/
4552
struct PLyObToDatum;
46-
typedef Datum (*PLyObToDatumFunc) (struct PLyObToDatum *, int32, PyObject *);
53+
typedef Datum (*PLyObToDatumFunc) (struct PLyObToDatum *arg, int32 typmod, PyObject *val, bool inarray);
4754

4855
typedef struct PLyObToDatum
4956
{
@@ -104,7 +111,7 @@ extern void PLy_output_tuple_funcs(PLyTypeInfo *arg, TupleDesc desc);
104111
extern void PLy_output_record_funcs(PLyTypeInfo *arg, TupleDesc desc);
105112

106113
/* conversion from Python objects to composite Datums */
107-
extern Datum PLyObject_ToCompositeDatum(PLyTypeInfo *info, TupleDesc desc, PyObject *plrv);
114+
extern Datum PLyObject_ToCompositeDatum(PLyTypeInfo *info, TupleDesc desc, PyObject *plrv, bool isarray);
108115

109116
/* conversion from heap tuples to Python dictionaries */
110117
extern PyObject *PLyDict_FromTuple(PLyTypeInfo *info, HeapTuple tuple, TupleDesc desc);

src/pl/plpython/sql/plpython_composite.sql

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,3 +213,12 @@ CREATE FUNCTION composite_type_as_list() RETURNS type_record[] AS $$
213213
return [[('first', 1), ('second', 1)], [('first', 2), ('second', 2)], [('first', 3), ('second', 3)]];
214214
$$ LANGUAGE plpythonu;
215215
SELECT * FROM composite_type_as_list();
216+
217+
-- Starting with PostgreSQL 10, a composite type in an array cannot be
218+
-- represented as a Python list, because it's ambiguous with multi-dimensional
219+
-- arrays. So this throws an error now. The error should contain a useful hint
220+
-- on the issue.
221+
CREATE FUNCTION composite_type_as_list_broken() RETURNS type_record[] AS $$
222+
return [['first', 1]];
223+
$$ LANGUAGE plpythonu;
224+
SELECT * FROM composite_type_as_list_broken();

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy