diff options
-rw-r--r-- | contrib/hstore_plpython/expected/hstore_plpython.out | 20 | ||||
-rw-r--r-- | contrib/hstore_plpython/sql/hstore_plpython.sql | 16 | ||||
-rw-r--r-- | src/backend/utils/cache/typcache.c | 7 | ||||
-rw-r--r-- | src/include/utils/typcache.h | 5 | ||||
-rw-r--r-- | src/pl/plpython/expected/plpython_types.out | 128 | ||||
-rw-r--r-- | src/pl/plpython/expected/plpython_types_3.out | 128 | ||||
-rw-r--r-- | src/pl/plpython/plpy_cursorobject.c | 76 | ||||
-rw-r--r-- | src/pl/plpython/plpy_cursorobject.h | 2 | ||||
-rw-r--r-- | src/pl/plpython/plpy_exec.c | 173 | ||||
-rw-r--r-- | src/pl/plpython/plpy_main.c | 7 | ||||
-rw-r--r-- | src/pl/plpython/plpy_planobject.h | 2 | ||||
-rw-r--r-- | src/pl/plpython/plpy_procedure.c | 148 | ||||
-rw-r--r-- | src/pl/plpython/plpy_procedure.h | 8 | ||||
-rw-r--r-- | src/pl/plpython/plpy_spi.c | 84 | ||||
-rw-r--r-- | src/pl/plpython/plpy_typeio.c | 1192 | ||||
-rw-r--r-- | src/pl/plpython/plpy_typeio.h | 220 | ||||
-rw-r--r-- | src/pl/plpython/sql/plpython_types.sql | 91 |
17 files changed, 1378 insertions, 929 deletions
diff --git a/contrib/hstore_plpython/expected/hstore_plpython.out b/contrib/hstore_plpython/expected/hstore_plpython.out index df49cd5f373..1ab5feea93d 100644 --- a/contrib/hstore_plpython/expected/hstore_plpython.out +++ b/contrib/hstore_plpython/expected/hstore_plpython.out @@ -68,12 +68,30 @@ AS $$ val = [{'a': 1, 'b': 'boo', 'c': None}, {'d': 2}] return val $$; - SELECT test2arr(); +SELECT test2arr(); test2arr -------------------------------------------------------------- {"\"a\"=>\"1\", \"b\"=>\"boo\", \"c\"=>NULL","\"d\"=>\"2\""} (1 row) +-- test python -> domain over hstore +CREATE DOMAIN hstore_foo AS hstore CHECK(VALUE ? 'foo'); +CREATE FUNCTION test2dom(fn text) RETURNS hstore_foo +LANGUAGE plpythonu +TRANSFORM FOR TYPE hstore +AS $$ +return {'a': 1, fn: 'boo', 'c': None} +$$; +SELECT test2dom('foo'); + test2dom +----------------------------------- + "a"=>"1", "c"=>NULL, "foo"=>"boo" +(1 row) + +SELECT test2dom('bar'); -- fail +ERROR: value for domain hstore_foo violates check constraint "hstore_foo_check" +CONTEXT: while creating return value +PL/Python function "test2dom" -- test as part of prepare/execute CREATE FUNCTION test3() RETURNS void LANGUAGE plpythonu diff --git a/contrib/hstore_plpython/sql/hstore_plpython.sql b/contrib/hstore_plpython/sql/hstore_plpython.sql index 911bbd67fed..2c54ee6aaad 100644 --- a/contrib/hstore_plpython/sql/hstore_plpython.sql +++ b/contrib/hstore_plpython/sql/hstore_plpython.sql @@ -60,7 +60,21 @@ val = [{'a': 1, 'b': 'boo', 'c': None}, {'d': 2}] return val $$; - SELECT test2arr(); +SELECT test2arr(); + + +-- test python -> domain over hstore +CREATE DOMAIN hstore_foo AS hstore CHECK(VALUE ? 'foo'); + +CREATE FUNCTION test2dom(fn text) RETURNS hstore_foo +LANGUAGE plpythonu +TRANSFORM FOR TYPE hstore +AS $$ +return {'a': 1, fn: 'boo', 'c': None} +$$; + +SELECT test2dom('foo'); +SELECT test2dom('bar'); -- fail -- test as part of prepare/execute diff --git a/src/backend/utils/cache/typcache.c b/src/backend/utils/cache/typcache.c index 7aadc5d6ef7..f6450c402c3 100644 --- a/src/backend/utils/cache/typcache.c +++ b/src/backend/utils/cache/typcache.c @@ -377,6 +377,7 @@ lookup_type_cache(Oid type_id, int flags) typentry->typstorage = typtup->typstorage; typentry->typtype = typtup->typtype; typentry->typrelid = typtup->typrelid; + typentry->typelem = typtup->typelem; /* If it's a domain, immediately thread it into the domain cache list */ if (typentry->typtype == TYPTYPE_DOMAIN) @@ -791,6 +792,12 @@ load_typcache_tupdesc(TypeCacheEntry *typentry) Assert(typentry->tupDesc->tdrefcount > 0); typentry->tupDesc->tdrefcount++; + /* + * In future, we could take some pains to not increment the seqno if the + * tupdesc didn't really change; but for now it's not worth it. + */ + typentry->tupDescSeqNo++; + relation_close(rel, AccessShareLock); } diff --git a/src/include/utils/typcache.h b/src/include/utils/typcache.h index ea799a8894f..c203dabbd0a 100644 --- a/src/include/utils/typcache.h +++ b/src/include/utils/typcache.h @@ -40,6 +40,7 @@ typedef struct TypeCacheEntry char typstorage; char typtype; Oid typrelid; + Oid typelem; /* * Information obtained from opfamily entries @@ -75,9 +76,11 @@ typedef struct TypeCacheEntry /* * Tuple descriptor if it's a composite type (row type). NULL if not * composite or information hasn't yet been requested. (NOTE: this is a - * reference-counted tupledesc.) + * reference-counted tupledesc.) To simplify caching dependent info, + * tupDescSeqNo is incremented each time tupDesc is rebuilt in a session. */ TupleDesc tupDesc; + int64 tupDescSeqNo; /* * Fields computed when TYPECACHE_RANGE_INFO is requested. Zeroes if not diff --git a/src/pl/plpython/expected/plpython_types.out b/src/pl/plpython/expected/plpython_types.out index 893de301dda..eda965a9e0d 100644 --- a/src/pl/plpython/expected/plpython_types.out +++ b/src/pl/plpython/expected/plpython_types.out @@ -765,6 +765,76 @@ SELECT * FROM test_type_conversion_array_domain_check_violation(); ERROR: value for domain ordered_pair_domain violates check constraint "ordered_pair_domain_check" CONTEXT: while creating return value PL/Python function "test_type_conversion_array_domain_check_violation" +-- +-- Arrays of domains +-- +CREATE FUNCTION test_read_uint2_array(x uint2[]) RETURNS uint2 AS $$ +plpy.info(x, type(x)) +return x[0] +$$ LANGUAGE plpythonu; +select test_read_uint2_array(array[1::uint2]); +INFO: ([1], <type 'list'>) + test_read_uint2_array +----------------------- + 1 +(1 row) + +CREATE FUNCTION test_build_uint2_array(x int2) RETURNS uint2[] AS $$ +return [x, x] +$$ LANGUAGE plpythonu; +select test_build_uint2_array(1::int2); + test_build_uint2_array +------------------------ + {1,1} +(1 row) + +select test_build_uint2_array(-1::int2); -- fail +ERROR: value for domain uint2 violates check constraint "uint2_check" +CONTEXT: while creating return value +PL/Python function "test_build_uint2_array" +-- +-- ideally this would work, but for now it doesn't, because the return value +-- is [[2,4], [2,4]] which our conversion code thinks should become a 2-D +-- integer array, not an array of arrays. +-- +CREATE FUNCTION test_type_conversion_domain_array(x integer[]) + RETURNS ordered_pair_domain[] AS $$ +return [x, x] +$$ LANGUAGE plpythonu; +select test_type_conversion_domain_array(array[2,4]); +ERROR: return value of function with array return type is not a Python sequence +CONTEXT: while creating return value +PL/Python function "test_type_conversion_domain_array" +select test_type_conversion_domain_array(array[4,2]); -- fail +ERROR: return value of function with array return type is not a Python sequence +CONTEXT: while creating return value +PL/Python function "test_type_conversion_domain_array" +CREATE FUNCTION test_type_conversion_domain_array2(x ordered_pair_domain) + RETURNS integer AS $$ +plpy.info(x, type(x)) +return x[1] +$$ LANGUAGE plpythonu; +select test_type_conversion_domain_array2(array[2,4]); +INFO: ([2, 4], <type 'list'>) + test_type_conversion_domain_array2 +------------------------------------ + 4 +(1 row) + +select test_type_conversion_domain_array2(array[4,2]); -- fail +ERROR: value for domain ordered_pair_domain violates check constraint "ordered_pair_domain_check" +CREATE FUNCTION test_type_conversion_array_domain_array(x ordered_pair_domain[]) + RETURNS ordered_pair_domain AS $$ +plpy.info(x, type(x)) +return x[0] +$$ LANGUAGE plpythonu; +select test_type_conversion_array_domain_array(array[array[2,4]::ordered_pair_domain]); +INFO: ([[2, 4]], <type 'list'>) + test_type_conversion_array_domain_array +----------------------------------------- + {2,4} +(1 row) + --- --- Composite types --- @@ -821,6 +891,64 @@ SELECT test_composite_type_input(row(1, 2)); (1 row) -- +-- Domains within composite +-- +CREATE TYPE nnint_container AS (f1 int, f2 nnint); +CREATE FUNCTION nnint_test(x int, y int) RETURNS nnint_container AS $$ +return {'f1': x, 'f2': y} +$$ LANGUAGE plpythonu; +SELECT nnint_test(null, 3); + nnint_test +------------ + (,3) +(1 row) + +SELECT nnint_test(3, null); -- fail +ERROR: value for domain nnint violates check constraint "nnint_check" +CONTEXT: while creating return value +PL/Python function "nnint_test" +-- +-- Domains of composite +-- +CREATE DOMAIN ordered_named_pair AS named_pair_2 CHECK((VALUE).i <= (VALUE).j); +CREATE FUNCTION read_ordered_named_pair(p ordered_named_pair) RETURNS integer AS $$ +return p['i'] + p['j'] +$$ LANGUAGE plpythonu; +SELECT read_ordered_named_pair(row(1, 2)); + read_ordered_named_pair +------------------------- + 3 +(1 row) + +SELECT read_ordered_named_pair(row(2, 1)); -- fail +ERROR: value for domain ordered_named_pair violates check constraint "ordered_named_pair_check" +CREATE FUNCTION build_ordered_named_pair(i int, j int) RETURNS ordered_named_pair AS $$ +return {'i': i, 'j': j} +$$ LANGUAGE plpythonu; +SELECT build_ordered_named_pair(1,2); + build_ordered_named_pair +-------------------------- + (1,2) +(1 row) + +SELECT build_ordered_named_pair(2,1); -- fail +ERROR: value for domain ordered_named_pair violates check constraint "ordered_named_pair_check" +CONTEXT: while creating return value +PL/Python function "build_ordered_named_pair" +CREATE FUNCTION build_ordered_named_pairs(i int, j int) RETURNS ordered_named_pair[] AS $$ +return [{'i': i, 'j': j}, {'i': i, 'j': j+1}] +$$ LANGUAGE plpythonu; +SELECT build_ordered_named_pairs(1,2); + build_ordered_named_pairs +--------------------------- + {"(1,2)","(1,3)"} +(1 row) + +SELECT build_ordered_named_pairs(2,1); -- fail +ERROR: value for domain ordered_named_pair violates check constraint "ordered_named_pair_check" +CONTEXT: while creating return value +PL/Python function "build_ordered_named_pairs" +-- -- Prepared statements -- CREATE OR REPLACE FUNCTION test_prep_bool_input() RETURNS int diff --git a/src/pl/plpython/expected/plpython_types_3.out b/src/pl/plpython/expected/plpython_types_3.out index 2d853bd5731..69f958cbf28 100644 --- a/src/pl/plpython/expected/plpython_types_3.out +++ b/src/pl/plpython/expected/plpython_types_3.out @@ -765,6 +765,76 @@ SELECT * FROM test_type_conversion_array_domain_check_violation(); ERROR: value for domain ordered_pair_domain violates check constraint "ordered_pair_domain_check" CONTEXT: while creating return value PL/Python function "test_type_conversion_array_domain_check_violation" +-- +-- Arrays of domains +-- +CREATE FUNCTION test_read_uint2_array(x uint2[]) RETURNS uint2 AS $$ +plpy.info(x, type(x)) +return x[0] +$$ LANGUAGE plpythonu; +select test_read_uint2_array(array[1::uint2]); +INFO: ([1], <class 'list'>) + test_read_uint2_array +----------------------- + 1 +(1 row) + +CREATE FUNCTION test_build_uint2_array(x int2) RETURNS uint2[] AS $$ +return [x, x] +$$ LANGUAGE plpythonu; +select test_build_uint2_array(1::int2); + test_build_uint2_array +------------------------ + {1,1} +(1 row) + +select test_build_uint2_array(-1::int2); -- fail +ERROR: value for domain uint2 violates check constraint "uint2_check" +CONTEXT: while creating return value +PL/Python function "test_build_uint2_array" +-- +-- ideally this would work, but for now it doesn't, because the return value +-- is [[2,4], [2,4]] which our conversion code thinks should become a 2-D +-- integer array, not an array of arrays. +-- +CREATE FUNCTION test_type_conversion_domain_array(x integer[]) + RETURNS ordered_pair_domain[] AS $$ +return [x, x] +$$ LANGUAGE plpythonu; +select test_type_conversion_domain_array(array[2,4]); +ERROR: return value of function with array return type is not a Python sequence +CONTEXT: while creating return value +PL/Python function "test_type_conversion_domain_array" +select test_type_conversion_domain_array(array[4,2]); -- fail +ERROR: return value of function with array return type is not a Python sequence +CONTEXT: while creating return value +PL/Python function "test_type_conversion_domain_array" +CREATE FUNCTION test_type_conversion_domain_array2(x ordered_pair_domain) + RETURNS integer AS $$ +plpy.info(x, type(x)) +return x[1] +$$ LANGUAGE plpythonu; +select test_type_conversion_domain_array2(array[2,4]); +INFO: ([2, 4], <class 'list'>) + test_type_conversion_domain_array2 +------------------------------------ + 4 +(1 row) + +select test_type_conversion_domain_array2(array[4,2]); -- fail +ERROR: value for domain ordered_pair_domain violates check constraint "ordered_pair_domain_check" +CREATE FUNCTION test_type_conversion_array_domain_array(x ordered_pair_domain[]) + RETURNS ordered_pair_domain AS $$ +plpy.info(x, type(x)) +return x[0] +$$ LANGUAGE plpythonu; +select test_type_conversion_array_domain_array(array[array[2,4]::ordered_pair_domain]); +INFO: ([[2, 4]], <class 'list'>) + test_type_conversion_array_domain_array +----------------------------------------- + {2,4} +(1 row) + --- --- Composite types --- @@ -821,6 +891,64 @@ SELECT test_composite_type_input(row(1, 2)); (1 row) -- +-- Domains within composite +-- +CREATE TYPE nnint_container AS (f1 int, f2 nnint); +CREATE FUNCTION nnint_test(x int, y int) RETURNS nnint_container AS $$ +return {'f1': x, 'f2': y} +$$ LANGUAGE plpythonu; +SELECT nnint_test(null, 3); + nnint_test +------------ + (,3) +(1 row) + +SELECT nnint_test(3, null); -- fail +ERROR: value for domain nnint violates check constraint "nnint_check" +CONTEXT: while creating return value +PL/Python function "nnint_test" +-- +-- Domains of composite +-- +CREATE DOMAIN ordered_named_pair AS named_pair_2 CHECK((VALUE).i <= (VALUE).j); +CREATE FUNCTION read_ordered_named_pair(p ordered_named_pair) RETURNS integer AS $$ +return p['i'] + p['j'] +$$ LANGUAGE plpythonu; +SELECT read_ordered_named_pair(row(1, 2)); + read_ordered_named_pair +------------------------- + 3 +(1 row) + +SELECT read_ordered_named_pair(row(2, 1)); -- fail +ERROR: value for domain ordered_named_pair violates check constraint "ordered_named_pair_check" +CREATE FUNCTION build_ordered_named_pair(i int, j int) RETURNS ordered_named_pair AS $$ +return {'i': i, 'j': j} +$$ LANGUAGE plpythonu; +SELECT build_ordered_named_pair(1,2); + build_ordered_named_pair +-------------------------- + (1,2) +(1 row) + +SELECT build_ordered_named_pair(2,1); -- fail +ERROR: value for domain ordered_named_pair violates check constraint "ordered_named_pair_check" +CONTEXT: while creating return value +PL/Python function "build_ordered_named_pair" +CREATE FUNCTION build_ordered_named_pairs(i int, j int) RETURNS ordered_named_pair[] AS $$ +return [{'i': i, 'j': j}, {'i': i, 'j': j+1}] +$$ LANGUAGE plpythonu; +SELECT build_ordered_named_pairs(1,2); + build_ordered_named_pairs +--------------------------- + {"(1,2)","(1,3)"} +(1 row) + +SELECT build_ordered_named_pairs(2,1); -- fail +ERROR: value for domain ordered_named_pair violates check constraint "ordered_named_pair_check" +CONTEXT: while creating return value +PL/Python function "build_ordered_named_pairs" +-- -- Prepared statements -- CREATE OR REPLACE FUNCTION test_prep_bool_input() RETURNS int diff --git a/src/pl/plpython/plpy_cursorobject.c b/src/pl/plpython/plpy_cursorobject.c index 0108471bfe2..10ca786fbc2 100644 --- a/src/pl/plpython/plpy_cursorobject.c +++ b/src/pl/plpython/plpy_cursorobject.c @@ -9,6 +9,7 @@ #include <limits.h> #include "access/xact.h" +#include "catalog/pg_type.h" #include "mb/pg_wchar.h" #include "utils/memutils.h" @@ -106,6 +107,7 @@ static PyObject * PLy_cursor_query(const char *query) { PLyCursorObject *cursor; + PLyExecutionContext *exec_ctx = PLy_current_execution_context(); volatile MemoryContext oldcontext; volatile ResourceOwner oldowner; @@ -116,7 +118,11 @@ PLy_cursor_query(const char *query) cursor->mcxt = AllocSetContextCreate(TopMemoryContext, "PL/Python cursor context", ALLOCSET_DEFAULT_SIZES); - PLy_typeinfo_init(&cursor->result, cursor->mcxt); + + /* Initialize for converting result tuples to Python */ + PLy_input_setup_func(&cursor->result, cursor->mcxt, + RECORDOID, -1, + exec_ctx->curr_proc); oldcontext = CurrentMemoryContext; oldowner = CurrentResourceOwner; @@ -125,7 +131,6 @@ PLy_cursor_query(const char *query) PG_TRY(); { - PLyExecutionContext *exec_ctx = PLy_current_execution_context(); SPIPlanPtr plan; Portal portal; @@ -166,6 +171,7 @@ PLy_cursor_plan(PyObject *ob, PyObject *args) volatile int nargs; int i; PLyPlanObject *plan; + PLyExecutionContext *exec_ctx = PLy_current_execution_context(); volatile MemoryContext oldcontext; volatile ResourceOwner oldowner; @@ -208,7 +214,11 @@ PLy_cursor_plan(PyObject *ob, PyObject *args) cursor->mcxt = AllocSetContextCreate(TopMemoryContext, "PL/Python cursor context", ALLOCSET_DEFAULT_SIZES); - PLy_typeinfo_init(&cursor->result, cursor->mcxt); + + /* Initialize for converting result tuples to Python */ + PLy_input_setup_func(&cursor->result, cursor->mcxt, + RECORDOID, -1, + exec_ctx->curr_proc); oldcontext = CurrentMemoryContext; oldowner = CurrentResourceOwner; @@ -217,7 +227,6 @@ PLy_cursor_plan(PyObject *ob, PyObject *args) PG_TRY(); { - PLyExecutionContext *exec_ctx = PLy_current_execution_context(); Portal portal; char *volatile nulls; volatile int j; @@ -229,39 +238,24 @@ PLy_cursor_plan(PyObject *ob, PyObject *args) for (j = 0; j < nargs; j++) { + PLyObToDatum *arg = &plan->args[j]; PyObject *elem; elem = PySequence_GetItem(args, j); - if (elem != Py_None) + PG_TRY(); { - PG_TRY(); - { - plan->values[j] = - plan->args[j].out.d.func(&(plan->args[j].out.d), - -1, - elem, - false); - } - PG_CATCH(); - { - Py_DECREF(elem); - PG_RE_THROW(); - } - PG_END_TRY(); + bool isnull; - Py_DECREF(elem); - nulls[j] = ' '; + plan->values[j] = PLy_output_convert(arg, elem, &isnull); + nulls[j] = isnull ? 'n' : ' '; } - else + PG_CATCH(); { Py_DECREF(elem); - plan->values[j] = - InputFunctionCall(&(plan->args[j].out.d.typfunc), - NULL, - plan->args[j].out.d.typioparam, - -1); - nulls[j] = 'n'; + PG_RE_THROW(); } + PG_END_TRY(); + Py_DECREF(elem); } portal = SPI_cursor_open(NULL, plan->plan, plan->values, nulls, @@ -281,7 +275,7 @@ PLy_cursor_plan(PyObject *ob, PyObject *args) /* cleanup plan->values array */ for (k = 0; k < nargs; k++) { - if (!plan->args[k].out.d.typbyval && + if (!plan->args[k].typbyval && (plan->values[k] != PointerGetDatum(NULL))) { pfree(DatumGetPointer(plan->values[k])); @@ -298,7 +292,7 @@ PLy_cursor_plan(PyObject *ob, PyObject *args) for (i = 0; i < nargs; i++) { - if (!plan->args[i].out.d.typbyval && + if (!plan->args[i].typbyval && (plan->values[i] != PointerGetDatum(NULL))) { pfree(DatumGetPointer(plan->values[i])); @@ -339,6 +333,7 @@ PLy_cursor_iternext(PyObject *self) { PLyCursorObject *cursor; PyObject *ret; + PLyExecutionContext *exec_ctx = PLy_current_execution_context(); volatile MemoryContext oldcontext; volatile ResourceOwner oldowner; Portal portal; @@ -374,11 +369,11 @@ PLy_cursor_iternext(PyObject *self) } else { - if (cursor->result.is_rowtype != 1) - PLy_input_tuple_funcs(&cursor->result, SPI_tuptable->tupdesc); + PLy_input_setup_tuple(&cursor->result, SPI_tuptable->tupdesc, + exec_ctx->curr_proc); - ret = PLyDict_FromTuple(&cursor->result, SPI_tuptable->vals[0], - SPI_tuptable->tupdesc); + ret = PLy_input_from_tuple(&cursor->result, SPI_tuptable->vals[0], + SPI_tuptable->tupdesc); } SPI_freetuptable(SPI_tuptable); @@ -401,6 +396,7 @@ PLy_cursor_fetch(PyObject *self, PyObject *args) PLyCursorObject *cursor; int count; PLyResultObject *ret; + PLyExecutionContext *exec_ctx = PLy_current_execution_context(); volatile MemoryContext oldcontext; volatile ResourceOwner oldowner; Portal portal; @@ -437,9 +433,6 @@ PLy_cursor_fetch(PyObject *self, PyObject *args) { SPI_cursor_fetch(portal, true, count); - if (cursor->result.is_rowtype != 1) - PLy_input_tuple_funcs(&cursor->result, SPI_tuptable->tupdesc); - Py_DECREF(ret->status); ret->status = PyInt_FromLong(SPI_OK_FETCH); @@ -465,11 +458,14 @@ PLy_cursor_fetch(PyObject *self, PyObject *args) Py_DECREF(ret->rows); ret->rows = PyList_New(SPI_processed); + PLy_input_setup_tuple(&cursor->result, SPI_tuptable->tupdesc, + exec_ctx->curr_proc); + for (i = 0; i < SPI_processed; i++) { - PyObject *row = PLyDict_FromTuple(&cursor->result, - SPI_tuptable->vals[i], - SPI_tuptable->tupdesc); + PyObject *row = PLy_input_from_tuple(&cursor->result, + SPI_tuptable->vals[i], + SPI_tuptable->tupdesc); PyList_SetItem(ret->rows, i, row); } diff --git a/src/pl/plpython/plpy_cursorobject.h b/src/pl/plpython/plpy_cursorobject.h index 018b169cbf2..e4d2c0ed253 100644 --- a/src/pl/plpython/plpy_cursorobject.h +++ b/src/pl/plpython/plpy_cursorobject.h @@ -12,7 +12,7 @@ typedef struct PLyCursorObject { PyObject_HEAD char *portalname; - PLyTypeInfo result; + PLyDatumToOb result; bool closed; MemoryContext mcxt; } PLyCursorObject; diff --git a/src/pl/plpython/plpy_exec.c b/src/pl/plpython/plpy_exec.c index 26f61dd0f37..02d7d2ad5f8 100644 --- a/src/pl/plpython/plpy_exec.c +++ b/src/pl/plpython/plpy_exec.c @@ -202,7 +202,7 @@ PLy_exec_function(FunctionCallInfo fcinfo, PLyProcedure *proc) * return value as a special "void datum" rather than NULL (as is the * case for non-void-returning functions). */ - if (proc->result.out.d.typoid == VOIDOID) + if (proc->result.typoid == VOIDOID) { if (plrv != Py_None) ereport(ERROR, @@ -212,48 +212,22 @@ PLy_exec_function(FunctionCallInfo fcinfo, PLyProcedure *proc) fcinfo->isnull = false; rv = (Datum) 0; } - else if (plrv == Py_None) + else if (plrv == Py_None && + srfstate && srfstate->iter == NULL) { - fcinfo->isnull = true; - /* * In a SETOF function, the iteration-ending null isn't a real * value; don't pass it through the input function, which might * complain. */ - if (srfstate && srfstate->iter == NULL) - rv = (Datum) 0; - else if (proc->result.is_rowtype < 1) - rv = InputFunctionCall(&proc->result.out.d.typfunc, - NULL, - proc->result.out.d.typioparam, - -1); - else - /* Tuple as None */ - rv = (Datum) NULL; - } - else if (proc->result.is_rowtype >= 1) - { - TupleDesc desc; - - /* make sure it's not an unnamed record */ - Assert((proc->result.out.d.typoid == RECORDOID && - proc->result.out.d.typmod != -1) || - (proc->result.out.d.typoid != RECORDOID && - proc->result.out.d.typmod == -1)); - - desc = lookup_rowtype_tupdesc(proc->result.out.d.typoid, - proc->result.out.d.typmod); - - rv = PLyObject_ToCompositeDatum(&proc->result, desc, plrv, false); - fcinfo->isnull = (rv == (Datum) NULL); - - ReleaseTupleDesc(desc); + fcinfo->isnull = true; + rv = (Datum) 0; } else { - fcinfo->isnull = false; - rv = (proc->result.out.d.func) (&proc->result.out.d, -1, plrv, false); + /* Normal conversion of result */ + rv = PLy_output_convert(&proc->result, plrv, + &fcinfo->isnull); } } PG_CATCH(); @@ -328,20 +302,32 @@ PLy_exec_trigger(FunctionCallInfo fcinfo, PLyProcedure *proc) PyObject *volatile plargs = NULL; PyObject *volatile plrv = NULL; TriggerData *tdata; + TupleDesc rel_descr; Assert(CALLED_AS_TRIGGER(fcinfo)); + tdata = (TriggerData *) fcinfo->context; /* - * Input/output conversion for trigger tuples. Use the result TypeInfo - * variable to store the tuple conversion info. We do this over again on - * each call to cover the possibility that the relation's tupdesc changed - * since the trigger was last called. PLy_input_tuple_funcs and - * PLy_output_tuple_funcs are responsible for not doing repetitive work. + * Input/output conversion for trigger tuples. We use the result and + * result_in fields to store the tuple conversion info. We do this over + * again on each call to cover the possibility that the relation's tupdesc + * changed since the trigger was last called. The PLy_xxx_setup_func + * calls should only happen once, but PLy_input_setup_tuple and + * PLy_output_setup_tuple are responsible for not doing repetitive work. */ - tdata = (TriggerData *) fcinfo->context; - - PLy_input_tuple_funcs(&(proc->result), tdata->tg_relation->rd_att); - PLy_output_tuple_funcs(&(proc->result), tdata->tg_relation->rd_att); + rel_descr = RelationGetDescr(tdata->tg_relation); + if (proc->result.typoid != rel_descr->tdtypeid) + PLy_output_setup_func(&proc->result, proc->mcxt, + rel_descr->tdtypeid, + rel_descr->tdtypmod, + proc); + if (proc->result_in.typoid != rel_descr->tdtypeid) + PLy_input_setup_func(&proc->result_in, proc->mcxt, + rel_descr->tdtypeid, + rel_descr->tdtypmod, + proc); + PLy_output_setup_tuple(&proc->result, rel_descr, proc); + PLy_input_setup_tuple(&proc->result_in, rel_descr, proc); PG_TRY(); { @@ -436,46 +422,12 @@ PLy_function_build_args(FunctionCallInfo fcinfo, PLyProcedure *proc) args = PyList_New(proc->nargs); for (i = 0; i < proc->nargs; i++) { - if (proc->args[i].is_rowtype > 0) - { - if (fcinfo->argnull[i]) - arg = NULL; - else - { - HeapTupleHeader td; - Oid tupType; - int32 tupTypmod; - TupleDesc tupdesc; - HeapTupleData tmptup; - - td = DatumGetHeapTupleHeader(fcinfo->arg[i]); - /* Extract rowtype info and find a tupdesc */ - tupType = HeapTupleHeaderGetTypeId(td); - tupTypmod = HeapTupleHeaderGetTypMod(td); - tupdesc = lookup_rowtype_tupdesc(tupType, tupTypmod); - - /* Set up I/O funcs if not done yet */ - if (proc->args[i].is_rowtype != 1) - PLy_input_tuple_funcs(&(proc->args[i]), tupdesc); - - /* Build a temporary HeapTuple control structure */ - tmptup.t_len = HeapTupleHeaderGetDatumLength(td); - tmptup.t_data = td; - - arg = PLyDict_FromTuple(&(proc->args[i]), &tmptup, tupdesc); - ReleaseTupleDesc(tupdesc); - } - } + PLyDatumToOb *arginfo = &proc->args[i]; + + if (fcinfo->argnull[i]) + arg = NULL; else - { - if (fcinfo->argnull[i]) - arg = NULL; - else - { - arg = (proc->args[i].in.d.func) (&(proc->args[i].in.d), - fcinfo->arg[i]); - } - } + arg = PLy_input_convert(arginfo, fcinfo->arg[i]); if (arg == NULL) { @@ -493,7 +445,7 @@ PLy_function_build_args(FunctionCallInfo fcinfo, PLyProcedure *proc) } /* Set up output conversion for functions returning RECORD */ - if (proc->result.out.d.typoid == RECORDOID) + if (proc->result.typoid == RECORDOID) { TupleDesc desc; @@ -504,7 +456,7 @@ PLy_function_build_args(FunctionCallInfo fcinfo, PLyProcedure *proc) "that cannot accept type record"))); /* cache the output conversion functions */ - PLy_output_record_funcs(&(proc->result), desc); + PLy_output_setup_record(&proc->result, desc, proc); } } PG_CATCH(); @@ -723,6 +675,7 @@ static PyObject * PLy_trigger_build_args(FunctionCallInfo fcinfo, PLyProcedure *proc, HeapTuple *rv) { TriggerData *tdata = (TriggerData *) fcinfo->context; + TupleDesc rel_descr = RelationGetDescr(tdata->tg_relation); PyObject *pltname, *pltevent, *pltwhen, @@ -790,8 +743,9 @@ PLy_trigger_build_args(FunctionCallInfo fcinfo, PLyProcedure *proc, HeapTuple *r pltevent = PyString_FromString("INSERT"); PyDict_SetItemString(pltdata, "old", Py_None); - pytnew = PLyDict_FromTuple(&(proc->result), tdata->tg_trigtuple, - tdata->tg_relation->rd_att); + pytnew = PLy_input_from_tuple(&proc->result_in, + tdata->tg_trigtuple, + rel_descr); PyDict_SetItemString(pltdata, "new", pytnew); Py_DECREF(pytnew); *rv = tdata->tg_trigtuple; @@ -801,8 +755,9 @@ PLy_trigger_build_args(FunctionCallInfo fcinfo, PLyProcedure *proc, HeapTuple *r pltevent = PyString_FromString("DELETE"); PyDict_SetItemString(pltdata, "new", Py_None); - pytold = PLyDict_FromTuple(&(proc->result), tdata->tg_trigtuple, - tdata->tg_relation->rd_att); + pytold = PLy_input_from_tuple(&proc->result_in, + tdata->tg_trigtuple, + rel_descr); PyDict_SetItemString(pltdata, "old", pytold); Py_DECREF(pytold); *rv = tdata->tg_trigtuple; @@ -811,12 +766,14 @@ PLy_trigger_build_args(FunctionCallInfo fcinfo, PLyProcedure *proc, HeapTuple *r { pltevent = PyString_FromString("UPDATE"); - pytnew = PLyDict_FromTuple(&(proc->result), tdata->tg_newtuple, - tdata->tg_relation->rd_att); + pytnew = PLy_input_from_tuple(&proc->result_in, + tdata->tg_newtuple, + rel_descr); PyDict_SetItemString(pltdata, "new", pytnew); Py_DECREF(pytnew); - pytold = PLyDict_FromTuple(&(proc->result), tdata->tg_trigtuple, - tdata->tg_relation->rd_att); + pytold = PLy_input_from_tuple(&proc->result_in, + tdata->tg_trigtuple, + rel_descr); PyDict_SetItemString(pltdata, "old", pytold); Py_DECREF(pytold); *rv = tdata->tg_newtuple; @@ -897,6 +854,9 @@ PLy_trigger_build_args(FunctionCallInfo fcinfo, PLyProcedure *proc, HeapTuple *r return pltdata; } +/* + * Apply changes requested by a MODIFY return from a trigger function. + */ static HeapTuple PLy_modify_tuple(PLyProcedure *proc, PyObject *pltd, TriggerData *tdata, HeapTuple otup) @@ -938,7 +898,7 @@ PLy_modify_tuple(PLyProcedure *proc, PyObject *pltd, TriggerData *tdata, plkeys = PyDict_Keys(plntup); nkeys = PyList_Size(plkeys); - tupdesc = tdata->tg_relation->rd_att; + tupdesc = RelationGetDescr(tdata->tg_relation); modvalues = (Datum *) palloc0(tupdesc->natts * sizeof(Datum)); modnulls = (bool *) palloc0(tupdesc->natts * sizeof(bool)); @@ -950,7 +910,6 @@ PLy_modify_tuple(PLyProcedure *proc, PyObject *pltd, TriggerData *tdata, char *plattstr; int attn; PLyObToDatum *att; - Form_pg_attribute attr; platt = PyList_GetItem(plkeys, i); if (PyString_Check(platt)) @@ -975,7 +934,6 @@ PLy_modify_tuple(PLyProcedure *proc, PyObject *pltd, TriggerData *tdata, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("cannot set system attribute \"%s\"", plattstr))); - att = &proc->result.out.r.atts[attn - 1]; plval = PyDict_GetItem(plntup, platt); if (plval == NULL) @@ -983,25 +941,12 @@ PLy_modify_tuple(PLyProcedure *proc, PyObject *pltd, TriggerData *tdata, Py_INCREF(plval); - attr = TupleDescAttr(tupdesc, attn - 1); - if (plval != Py_None) - { - modvalues[attn - 1] = - (att->func) (att, - attr->atttypmod, - plval, - false); - modnulls[attn - 1] = false; - } - else - { - modvalues[attn - 1] = - InputFunctionCall(&att->typfunc, - NULL, - att->typioparam, - attr->atttypmod); - modnulls[attn - 1] = true; - } + /* We assume proc->result is set up to convert tuples properly */ + att = &proc->result.u.tuple.atts[attn - 1]; + + modvalues[attn - 1] = PLy_output_convert(att, + plval, + &modnulls[attn - 1]); modrepls[attn - 1] = true; Py_DECREF(plval); diff --git a/src/pl/plpython/plpy_main.c b/src/pl/plpython/plpy_main.c index 7df50c09c82..29db90e4489 100644 --- a/src/pl/plpython/plpy_main.c +++ b/src/pl/plpython/plpy_main.c @@ -318,7 +318,12 @@ plpython_inline_handler(PG_FUNCTION_ARGS) ALLOCSET_DEFAULT_SIZES); proc.pyname = MemoryContextStrdup(proc.mcxt, "__plpython_inline_block"); proc.langid = codeblock->langOid; - proc.result.out.d.typoid = VOIDOID; + + /* + * This is currently sufficient to get PLy_exec_function to work, but + * someday we might need to be honest and use PLy_output_setup_func. + */ + proc.result.typoid = VOIDOID; /* * Push execution context onto stack. It is important that this get diff --git a/src/pl/plpython/plpy_planobject.h b/src/pl/plpython/plpy_planobject.h index 5adc957053f..729effb1631 100644 --- a/src/pl/plpython/plpy_planobject.h +++ b/src/pl/plpython/plpy_planobject.h @@ -16,7 +16,7 @@ typedef struct PLyPlanObject int nargs; Oid *types; Datum *values; - PLyTypeInfo *args; + PLyObToDatum *args; MemoryContext mcxt; } PLyPlanObject; diff --git a/src/pl/plpython/plpy_procedure.c b/src/pl/plpython/plpy_procedure.c index 26acc88b270..58d69882025 100644 --- a/src/pl/plpython/plpy_procedure.c +++ b/src/pl/plpython/plpy_procedure.c @@ -15,6 +15,7 @@ #include "utils/builtins.h" #include "utils/hsearch.h" #include "utils/inval.h" +#include "utils/lsyscache.h" #include "utils/memutils.h" #include "utils/syscache.h" @@ -29,7 +30,6 @@ static HTAB *PLy_procedure_cache = NULL; static PLyProcedure *PLy_procedure_create(HeapTuple procTup, Oid fn_oid, bool is_trigger); -static bool PLy_procedure_argument_valid(PLyTypeInfo *arg); static bool PLy_procedure_valid(PLyProcedure *proc, HeapTuple procTup); static char *PLy_procedure_munge_source(const char *name, const char *src); @@ -165,6 +165,7 @@ PLy_procedure_create(HeapTuple procTup, Oid fn_oid, bool is_trigger) *ptr = '_'; } + /* Create long-lived context that all procedure info will live in */ cxt = AllocSetContextCreate(TopMemoryContext, procName, ALLOCSET_DEFAULT_SIZES); @@ -188,11 +189,9 @@ PLy_procedure_create(HeapTuple procTup, Oid fn_oid, bool is_trigger) proc->fn_tid = procTup->t_self; proc->fn_readonly = (procStruct->provolatile != PROVOLATILE_VOLATILE); proc->is_setof = procStruct->proretset; - PLy_typeinfo_init(&proc->result, proc->mcxt); proc->src = NULL; proc->argnames = NULL; - for (i = 0; i < FUNC_MAX_ARGS; i++) - PLy_typeinfo_init(&proc->args[i], proc->mcxt); + proc->args = NULL; proc->nargs = 0; proc->langid = procStruct->prolang; protrftypes_datum = SysCacheGetAttr(PROCOID, procTup, @@ -211,50 +210,48 @@ PLy_procedure_create(HeapTuple procTup, Oid fn_oid, bool is_trigger) */ if (!is_trigger) { + Oid rettype = procStruct->prorettype; HeapTuple rvTypeTup; Form_pg_type rvTypeStruct; - rvTypeTup = SearchSysCache1(TYPEOID, - ObjectIdGetDatum(procStruct->prorettype)); + rvTypeTup = SearchSysCache1(TYPEOID, ObjectIdGetDatum(rettype)); if (!HeapTupleIsValid(rvTypeTup)) - elog(ERROR, "cache lookup failed for type %u", - procStruct->prorettype); + elog(ERROR, "cache lookup failed for type %u", rettype); rvTypeStruct = (Form_pg_type) GETSTRUCT(rvTypeTup); /* Disallow pseudotype result, except for void or record */ if (rvTypeStruct->typtype == TYPTYPE_PSEUDO) { - if (procStruct->prorettype == TRIGGEROID) + if (rettype == VOIDOID || + rettype == RECORDOID) + /* okay */ ; + else if (rettype == TRIGGEROID || rettype == EVTTRIGGEROID) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("trigger functions can only be called as triggers"))); - else if (procStruct->prorettype != VOIDOID && - procStruct->prorettype != RECORDOID) + else ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("PL/Python functions cannot return type %s", - format_type_be(procStruct->prorettype)))); + format_type_be(rettype)))); } - if (rvTypeStruct->typtype == TYPTYPE_COMPOSITE || - procStruct->prorettype == RECORDOID) - { - /* - * Tuple: set up later, during first call to - * PLy_function_handler - */ - proc->result.out.d.typoid = procStruct->prorettype; - proc->result.out.d.typmod = -1; - proc->result.is_rowtype = 2; - } - else - { - /* do the real work */ - PLy_output_datum_func(&proc->result, rvTypeTup, proc->langid, proc->trftypes); - } + /* set up output function for procedure result */ + PLy_output_setup_func(&proc->result, proc->mcxt, + rettype, -1, proc); ReleaseSysCache(rvTypeTup); } + else + { + /* + * In a trigger function, we use proc->result and proc->result_in + * for converting tuples, but we don't yet have enough info to set + * them up. PLy_exec_trigger will deal with it. + */ + proc->result.typoid = InvalidOid; + proc->result_in.typoid = InvalidOid; + } /* * Now get information required for input conversion of the @@ -287,7 +284,10 @@ PLy_procedure_create(HeapTuple procTup, Oid fn_oid, bool is_trigger) } } + /* Allocate arrays for per-input-argument data */ proc->argnames = (char **) palloc0(sizeof(char *) * proc->nargs); + proc->args = (PLyDatumToOb *) palloc0(sizeof(PLyDatumToOb) * proc->nargs); + for (i = pos = 0; i < total; i++) { HeapTuple argTypeTup; @@ -306,28 +306,17 @@ PLy_procedure_create(HeapTuple procTup, Oid fn_oid, bool is_trigger) elog(ERROR, "cache lookup failed for type %u", types[i]); argTypeStruct = (Form_pg_type) GETSTRUCT(argTypeTup); - /* check argument type is OK, set up I/O function info */ - switch (argTypeStruct->typtype) - { - case TYPTYPE_PSEUDO: - /* Disallow pseudotype argument */ - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("PL/Python functions cannot accept type %s", - format_type_be(types[i])))); - break; - case TYPTYPE_COMPOSITE: - /* we'll set IO funcs at first call */ - proc->args[pos].is_rowtype = 2; - break; - default: - PLy_input_datum_func(&(proc->args[pos]), - types[i], - argTypeTup, - proc->langid, - proc->trftypes); - break; - } + /* disallow pseudotype arguments */ + if (argTypeStruct->typtype == TYPTYPE_PSEUDO) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("PL/Python functions cannot accept type %s", + format_type_be(types[i])))); + + /* set up I/O function info */ + PLy_input_setup_func(&proc->args[pos], proc->mcxt, + types[i], -1, /* typmod not known */ + proc); /* get argument name */ proc->argnames[pos] = names ? pstrdup(names[i]) : NULL; @@ -425,53 +414,11 @@ PLy_procedure_delete(PLyProcedure *proc) } /* - * Check if our cached information about a datatype is still valid - */ -static bool -PLy_procedure_argument_valid(PLyTypeInfo *arg) -{ - HeapTuple relTup; - bool valid; - - /* Nothing to cache unless type is composite */ - if (arg->is_rowtype != 1) - return true; - - /* - * Zero typ_relid means that we got called on an output argument of a - * function returning an unnamed record type; the info for it can't - * change. - */ - if (!OidIsValid(arg->typ_relid)) - return true; - - /* Else we should have some cached data */ - Assert(TransactionIdIsValid(arg->typrel_xmin)); - Assert(ItemPointerIsValid(&arg->typrel_tid)); - - /* Get the pg_class tuple for the data type */ - relTup = SearchSysCache1(RELOID, ObjectIdGetDatum(arg->typ_relid)); - if (!HeapTupleIsValid(relTup)) - elog(ERROR, "cache lookup failed for relation %u", arg->typ_relid); - - /* If it has changed, the cached data is not valid */ - valid = (arg->typrel_xmin == HeapTupleHeaderGetRawXmin(relTup->t_data) && - ItemPointerEquals(&arg->typrel_tid, &relTup->t_self)); - - ReleaseSysCache(relTup); - - return valid; -} - -/* * Decide whether a cached PLyProcedure struct is still valid */ static bool PLy_procedure_valid(PLyProcedure *proc, HeapTuple procTup) { - int i; - bool valid; - if (proc == NULL) return false; @@ -480,22 +427,7 @@ PLy_procedure_valid(PLyProcedure *proc, HeapTuple procTup) ItemPointerEquals(&proc->fn_tid, &procTup->t_self))) return false; - /* Else check the input argument datatypes */ - valid = true; - for (i = 0; i < proc->nargs; i++) - { - valid = PLy_procedure_argument_valid(&proc->args[i]); - - /* Short-circuit on first changed argument */ - if (!valid) - break; - } - - /* if the output type is composite, it might have changed */ - if (valid) - valid = PLy_procedure_argument_valid(&proc->result); - - return valid; + return true; } static char * diff --git a/src/pl/plpython/plpy_procedure.h b/src/pl/plpython/plpy_procedure.h index d05944fc398..cd1b87fdc3c 100644 --- a/src/pl/plpython/plpy_procedure.h +++ b/src/pl/plpython/plpy_procedure.h @@ -31,12 +31,12 @@ typedef struct PLyProcedure ItemPointerData fn_tid; bool fn_readonly; bool is_setof; /* true, if procedure returns result set */ - PLyTypeInfo result; /* also used to store info for trigger tuple - * type */ + PLyObToDatum result; /* Function result output conversion info */ + PLyDatumToOb result_in; /* For converting input tuples in a trigger */ char *src; /* textual procedure code, after mangling */ char **argnames; /* Argument names */ - PLyTypeInfo args[FUNC_MAX_ARGS]; - int nargs; + PLyDatumToOb *args; /* Argument input conversion info */ + int nargs; /* Number of elements in above arrays */ Oid langid; /* OID of plpython pg_language entry */ List *trftypes; /* OID list of transform types */ PyObject *code; /* compiled procedure code */ diff --git a/src/pl/plpython/plpy_spi.c b/src/pl/plpython/plpy_spi.c index 955769c5e32..69eb6b39f67 100644 --- a/src/pl/plpython/plpy_spi.c +++ b/src/pl/plpython/plpy_spi.c @@ -46,6 +46,7 @@ PLy_spi_prepare(PyObject *self, PyObject *args) PyObject *list = NULL; PyObject *volatile optr = NULL; char *query; + PLyExecutionContext *exec_ctx = PLy_current_execution_context(); volatile MemoryContext oldcontext; volatile ResourceOwner oldowner; volatile int nargs; @@ -71,9 +72,9 @@ PLy_spi_prepare(PyObject *self, PyObject *args) nargs = list ? PySequence_Length(list) : 0; plan->nargs = nargs; - plan->types = nargs ? palloc(sizeof(Oid) * nargs) : NULL; - plan->values = nargs ? palloc(sizeof(Datum) * nargs) : NULL; - plan->args = nargs ? palloc(sizeof(PLyTypeInfo) * nargs) : NULL; + plan->types = nargs ? palloc0(sizeof(Oid) * nargs) : NULL; + plan->values = nargs ? palloc0(sizeof(Datum) * nargs) : NULL; + plan->args = nargs ? palloc0(sizeof(PLyObToDatum) * nargs) : NULL; MemoryContextSwitchTo(oldcontext); @@ -85,22 +86,10 @@ PLy_spi_prepare(PyObject *self, PyObject *args) PG_TRY(); { int i; - PLyExecutionContext *exec_ctx = PLy_current_execution_context(); - - /* - * the other loop might throw an exception, if PLyTypeInfo member - * isn't properly initialized the Py_DECREF(plan) will go boom - */ - for (i = 0; i < nargs; i++) - { - PLy_typeinfo_init(&plan->args[i], plan->mcxt); - plan->values[i] = PointerGetDatum(NULL); - } for (i = 0; i < nargs; i++) { char *sptr; - HeapTuple typeTup; Oid typeId; int32 typmod; @@ -124,11 +113,6 @@ PLy_spi_prepare(PyObject *self, PyObject *args) parseTypeString(sptr, &typeId, &typmod, false); - typeTup = SearchSysCache1(TYPEOID, - ObjectIdGetDatum(typeId)); - if (!HeapTupleIsValid(typeTup)) - elog(ERROR, "cache lookup failed for type %u", typeId); - Py_DECREF(optr); /* @@ -138,8 +122,9 @@ PLy_spi_prepare(PyObject *self, PyObject *args) optr = NULL; plan->types[i] = typeId; - PLy_output_datum_func(&plan->args[i], typeTup, exec_ctx->curr_proc->langid, exec_ctx->curr_proc->trftypes); - ReleaseSysCache(typeTup); + PLy_output_setup_func(&plan->args[i], plan->mcxt, + typeId, typmod, + exec_ctx->curr_proc); } pg_verifymbstr(query, strlen(query), false); @@ -253,39 +238,24 @@ PLy_spi_execute_plan(PyObject *ob, PyObject *list, long limit) for (j = 0; j < nargs; j++) { + PLyObToDatum *arg = &plan->args[j]; PyObject *elem; elem = PySequence_GetItem(list, j); - if (elem != Py_None) + PG_TRY(); { - PG_TRY(); - { - plan->values[j] = - plan->args[j].out.d.func(&(plan->args[j].out.d), - -1, - elem, - false); - } - PG_CATCH(); - { - Py_DECREF(elem); - PG_RE_THROW(); - } - PG_END_TRY(); + bool isnull; - Py_DECREF(elem); - nulls[j] = ' '; + plan->values[j] = PLy_output_convert(arg, elem, &isnull); + nulls[j] = isnull ? 'n' : ' '; } - else + PG_CATCH(); { Py_DECREF(elem); - plan->values[j] = - InputFunctionCall(&(plan->args[j].out.d.typfunc), - NULL, - plan->args[j].out.d.typioparam, - -1); - nulls[j] = 'n'; + PG_RE_THROW(); } + PG_END_TRY(); + Py_DECREF(elem); } rv = SPI_execute_plan(plan->plan, plan->values, nulls, @@ -306,7 +276,7 @@ PLy_spi_execute_plan(PyObject *ob, PyObject *list, long limit) */ for (k = 0; k < nargs; k++) { - if (!plan->args[k].out.d.typbyval && + if (!plan->args[k].typbyval && (plan->values[k] != PointerGetDatum(NULL))) { pfree(DatumGetPointer(plan->values[k])); @@ -321,7 +291,7 @@ PLy_spi_execute_plan(PyObject *ob, PyObject *list, long limit) for (i = 0; i < nargs; i++) { - if (!plan->args[i].out.d.typbyval && + if (!plan->args[i].typbyval && (plan->values[i] != PointerGetDatum(NULL))) { pfree(DatumGetPointer(plan->values[i])); @@ -386,6 +356,7 @@ static PyObject * PLy_spi_execute_fetch_result(SPITupleTable *tuptable, uint64 rows, int status) { PLyResultObject *result; + PLyExecutionContext *exec_ctx = PLy_current_execution_context(); volatile MemoryContext oldcontext; result = (PLyResultObject *) PLy_result_new(); @@ -401,7 +372,7 @@ PLy_spi_execute_fetch_result(SPITupleTable *tuptable, uint64 rows, int status) } else if (status > 0 && tuptable != NULL) { - PLyTypeInfo args; + PLyDatumToOb ininfo; MemoryContext cxt; Py_DECREF(result->nrows); @@ -412,7 +383,10 @@ PLy_spi_execute_fetch_result(SPITupleTable *tuptable, uint64 rows, int status) cxt = AllocSetContextCreate(CurrentMemoryContext, "PL/Python temp context", ALLOCSET_DEFAULT_SIZES); - PLy_typeinfo_init(&args, cxt); + + /* Initialize for converting result tuples to Python */ + PLy_input_setup_func(&ininfo, cxt, RECORDOID, -1, + exec_ctx->curr_proc); oldcontext = CurrentMemoryContext; PG_TRY(); @@ -436,12 +410,14 @@ PLy_spi_execute_fetch_result(SPITupleTable *tuptable, uint64 rows, int status) Py_DECREF(result->rows); result->rows = PyList_New(rows); - PLy_input_tuple_funcs(&args, tuptable->tupdesc); + PLy_input_setup_tuple(&ininfo, tuptable->tupdesc, + exec_ctx->curr_proc); + for (i = 0; i < rows; i++) { - PyObject *row = PLyDict_FromTuple(&args, - tuptable->vals[i], - tuptable->tupdesc); + PyObject *row = PLy_input_from_tuple(&ininfo, + tuptable->vals[i], + tuptable->tupdesc); PyList_SetItem(result->rows, i, row); } diff --git a/src/pl/plpython/plpy_typeio.c b/src/pl/plpython/plpy_typeio.c index e4af8cc9ef0..ce1527072e4 100644 --- a/src/pl/plpython/plpy_typeio.c +++ b/src/pl/plpython/plpy_typeio.c @@ -7,19 +7,15 @@ #include "postgres.h" #include "access/htup_details.h" -#include "access/transam.h" #include "catalog/pg_type.h" #include "funcapi.h" #include "mb/pg_wchar.h" -#include "parser/parse_type.h" +#include "miscadmin.h" #include "utils/array.h" #include "utils/builtins.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/memutils.h" -#include "utils/numeric.h" -#include "utils/syscache.h" -#include "utils/typcache.h" #include "plpython.h" @@ -29,10 +25,6 @@ #include "plpy_main.h" -/* I/O function caching */ -static void PLy_input_datum_func2(PLyDatumToOb *arg, MemoryContext arg_mcxt, Oid typeOid, HeapTuple typeTup, Oid langid, List *trftypes); -static void PLy_output_datum_func2(PLyObToDatum *arg, MemoryContext arg_mcxt, HeapTuple typeTup, Oid langid, List *trftypes); - /* conversion from Datums to Python objects */ static PyObject *PLyBool_FromBool(PLyDatumToOb *arg, Datum d); static PyObject *PLyFloat_FromFloat4(PLyDatumToOb *arg, Datum d); @@ -43,361 +35,365 @@ static PyObject *PLyInt_FromInt32(PLyDatumToOb *arg, Datum d); static PyObject *PLyLong_FromInt64(PLyDatumToOb *arg, Datum d); static PyObject *PLyLong_FromOid(PLyDatumToOb *arg, Datum d); static PyObject *PLyBytes_FromBytea(PLyDatumToOb *arg, Datum d); -static PyObject *PLyString_FromDatum(PLyDatumToOb *arg, Datum d); +static PyObject *PLyString_FromScalar(PLyDatumToOb *arg, Datum d); static PyObject *PLyObject_FromTransform(PLyDatumToOb *arg, Datum d); static PyObject *PLyList_FromArray(PLyDatumToOb *arg, Datum d); static PyObject *PLyList_FromArray_recurse(PLyDatumToOb *elm, int *dims, int ndim, int dim, char **dataptr_p, bits8 **bitmap_p, int *bitmask_p); +static PyObject *PLyDict_FromComposite(PLyDatumToOb *arg, Datum d); +static PyObject *PLyDict_FromTuple(PLyDatumToOb *arg, HeapTuple tuple, TupleDesc desc); /* conversion from Python objects to Datums */ -static Datum PLyObject_ToBool(PLyObToDatum *arg, int32 typmod, PyObject *plrv, bool inarray); -static Datum PLyObject_ToBytea(PLyObToDatum *arg, int32 typmod, PyObject *plrv, bool inarray); -static Datum PLyObject_ToComposite(PLyObToDatum *arg, int32 typmod, PyObject *plrv, bool inarray); -static Datum PLyObject_ToDatum(PLyObToDatum *arg, int32 typmod, PyObject *plrv, bool inarray); -static Datum PLyObject_ToTransform(PLyObToDatum *arg, int32 typmod, PyObject *plrv, bool inarray); -static Datum PLySequence_ToArray(PLyObToDatum *arg, int32 typmod, PyObject *plrv, bool inarray); +static Datum PLyObject_ToBool(PLyObToDatum *arg, PyObject *plrv, + bool *isnull, bool inarray); +static Datum PLyObject_ToBytea(PLyObToDatum *arg, PyObject *plrv, + bool *isnull, bool inarray); +static Datum PLyObject_ToComposite(PLyObToDatum *arg, PyObject *plrv, + bool *isnull, bool inarray); +static Datum PLyObject_ToScalar(PLyObToDatum *arg, PyObject *plrv, + bool *isnull, bool inarray); +static Datum PLyObject_ToDomain(PLyObToDatum *arg, PyObject *plrv, + bool *isnull, bool inarray); +static Datum PLyObject_ToTransform(PLyObToDatum *arg, PyObject *plrv, + bool *isnull, bool inarray); +static Datum PLySequence_ToArray(PLyObToDatum *arg, PyObject *plrv, + bool *isnull, bool inarray); static void PLySequence_ToArray_recurse(PLyObToDatum *elm, PyObject *list, int *dims, int ndim, int dim, Datum *elems, bool *nulls, int *currelem); -/* conversion from Python objects to composite Datums (used by triggers and SRFs) */ -static Datum PLyString_ToComposite(PLyTypeInfo *info, TupleDesc desc, PyObject *string, bool inarray); -static Datum PLyMapping_ToComposite(PLyTypeInfo *info, TupleDesc desc, PyObject *mapping); -static Datum PLySequence_ToComposite(PLyTypeInfo *info, TupleDesc desc, PyObject *sequence); -static Datum PLyGenericObject_ToComposite(PLyTypeInfo *info, TupleDesc desc, PyObject *object, bool inarray); +/* conversion from Python objects to composite Datums */ +static Datum PLyString_ToComposite(PLyObToDatum *arg, PyObject *string, bool inarray); +static Datum PLyMapping_ToComposite(PLyObToDatum *arg, TupleDesc desc, PyObject *mapping); +static Datum PLySequence_ToComposite(PLyObToDatum *arg, TupleDesc desc, PyObject *sequence); +static Datum PLyGenericObject_ToComposite(PLyObToDatum *arg, TupleDesc desc, PyObject *object, bool inarray); -void -PLy_typeinfo_init(PLyTypeInfo *arg, MemoryContext mcxt) -{ - arg->is_rowtype = -1; - arg->in.r.natts = arg->out.r.natts = 0; - arg->in.r.atts = NULL; - arg->out.r.atts = NULL; - arg->typ_relid = InvalidOid; - arg->typrel_xmin = InvalidTransactionId; - ItemPointerSetInvalid(&arg->typrel_tid); - arg->mcxt = mcxt; -} /* * Conversion functions. Remember output from Python is input to * PostgreSQL, and vice versa. */ -void -PLy_input_datum_func(PLyTypeInfo *arg, Oid typeOid, HeapTuple typeTup, Oid langid, List *trftypes) + +/* + * Perform input conversion, given correctly-set-up state information. + * + * This is the outer-level entry point for any input conversion. Internally, + * the conversion functions recurse directly to each other. + */ +PyObject * +PLy_input_convert(PLyDatumToOb *arg, Datum val) { - if (arg->is_rowtype > 0) - elog(ERROR, "PLyTypeInfo struct is initialized for Tuple"); - arg->is_rowtype = 0; - PLy_input_datum_func2(&(arg->in.d), arg->mcxt, typeOid, typeTup, langid, trftypes); + PyObject *result; + PLyExecutionContext *exec_ctx = PLy_current_execution_context(); + MemoryContext scratch_context = PLy_get_scratch_context(exec_ctx); + MemoryContext oldcontext; + + /* + * Do the work in the scratch context to avoid leaking memory from the + * datatype output function calls. (The individual PLyDatumToObFunc + * functions can't reset the scratch context, because they recurse and an + * inner one might clobber data an outer one still needs. So we do it + * once at the outermost recursion level.) + * + * We reset the scratch context before, not after, each conversion cycle. + * This way we aren't on the hook to release a Python refcount on the + * result object in case MemoryContextReset throws an error. + */ + MemoryContextReset(scratch_context); + + oldcontext = MemoryContextSwitchTo(scratch_context); + + result = arg->func(arg, val); + + MemoryContextSwitchTo(oldcontext); + + return result; } -void -PLy_output_datum_func(PLyTypeInfo *arg, HeapTuple typeTup, Oid langid, List *trftypes) +/* + * Perform output conversion, given correctly-set-up state information. + * + * This is the outer-level entry point for any output conversion. Internally, + * the conversion functions recurse directly to each other. + * + * The result, as well as any cruft generated along the way, are in the + * current memory context. Caller is responsible for cleanup. + */ +Datum +PLy_output_convert(PLyObToDatum *arg, PyObject *val, bool *isnull) { - if (arg->is_rowtype > 0) - elog(ERROR, "PLyTypeInfo struct is initialized for a Tuple"); - arg->is_rowtype = 0; - PLy_output_datum_func2(&(arg->out.d), arg->mcxt, typeTup, langid, trftypes); + /* at outer level, we are not considering an array element */ + return arg->func(arg, val, isnull, false); } -void -PLy_input_tuple_funcs(PLyTypeInfo *arg, TupleDesc desc) +/* + * Transform a tuple into a Python dict object. + * + * Note: the tupdesc must match the one used to set up *arg. We could + * insist that this function lookup the tupdesc from what is in *arg, + * but in practice all callers have the right tupdesc available. + */ +PyObject * +PLy_input_from_tuple(PLyDatumToOb *arg, HeapTuple tuple, TupleDesc desc) { - int i; + PyObject *dict; PLyExecutionContext *exec_ctx = PLy_current_execution_context(); - MemoryContext oldcxt; + MemoryContext scratch_context = PLy_get_scratch_context(exec_ctx); + MemoryContext oldcontext; - oldcxt = MemoryContextSwitchTo(arg->mcxt); + /* + * As in PLy_input_convert, do the work in the scratch context. + */ + MemoryContextReset(scratch_context); - if (arg->is_rowtype == 0) - elog(ERROR, "PLyTypeInfo struct is initialized for a Datum"); - arg->is_rowtype = 1; + oldcontext = MemoryContextSwitchTo(scratch_context); - if (arg->in.r.natts != desc->natts) - { - if (arg->in.r.atts) - pfree(arg->in.r.atts); - arg->in.r.natts = desc->natts; - arg->in.r.atts = palloc0(desc->natts * sizeof(PLyDatumToOb)); - } + dict = PLyDict_FromTuple(arg, tuple, desc); - /* Can this be an unnamed tuple? If not, then an Assert would be enough */ - if (desc->tdtypmod != -1) - elog(ERROR, "received unnamed record type as input"); + MemoryContextSwitchTo(oldcontext); - Assert(OidIsValid(desc->tdtypeid)); + return dict; +} - /* - * RECORDOID means we got called to create input functions for a tuple - * fetched by plpy.execute or for an anonymous record type - */ - if (desc->tdtypeid != RECORDOID) - { - HeapTuple relTup; +/* + * Initialize, or re-initialize, per-column input info for a composite type. + * + * This is separate from PLy_input_setup_func() because in cases involving + * anonymous record types, we need to be passed the tupdesc explicitly. + * It's caller's responsibility that the tupdesc has adequate lifespan + * in such cases. If the tupdesc is for a named composite or registered + * record type, it does not need to be long-lived. + */ +void +PLy_input_setup_tuple(PLyDatumToOb *arg, TupleDesc desc, PLyProcedure *proc) +{ + int i; - /* Get the pg_class tuple corresponding to the type of the input */ - arg->typ_relid = typeidTypeRelid(desc->tdtypeid); - relTup = SearchSysCache1(RELOID, ObjectIdGetDatum(arg->typ_relid)); - if (!HeapTupleIsValid(relTup)) - elog(ERROR, "cache lookup failed for relation %u", arg->typ_relid); + /* We should be working on a previously-set-up struct */ + Assert(arg->func == PLyDict_FromComposite); - /* Remember XMIN and TID for later validation if cache is still OK */ - arg->typrel_xmin = HeapTupleHeaderGetRawXmin(relTup->t_data); - arg->typrel_tid = relTup->t_self; + /* Save pointer to tupdesc, but only if this is an anonymous record type */ + if (arg->typoid == RECORDOID && arg->typmod < 0) + arg->u.tuple.recdesc = desc; - ReleaseSysCache(relTup); + /* (Re)allocate atts array as needed */ + if (arg->u.tuple.natts != desc->natts) + { + if (arg->u.tuple.atts) + pfree(arg->u.tuple.atts); + arg->u.tuple.natts = desc->natts; + arg->u.tuple.atts = (PLyDatumToOb *) + MemoryContextAllocZero(arg->mcxt, + desc->natts * sizeof(PLyDatumToOb)); } + /* Fill the atts entries, except for dropped columns */ for (i = 0; i < desc->natts; i++) { - HeapTuple typeTup; Form_pg_attribute attr = TupleDescAttr(desc, i); + PLyDatumToOb *att = &arg->u.tuple.atts[i]; if (attr->attisdropped) continue; - if (arg->in.r.atts[i].typoid == attr->atttypid) + if (att->typoid == attr->atttypid && att->typmod == attr->atttypmod) continue; /* already set up this entry */ - typeTup = SearchSysCache1(TYPEOID, ObjectIdGetDatum(attr->atttypid)); - if (!HeapTupleIsValid(typeTup)) - elog(ERROR, "cache lookup failed for type %u", - attr->atttypid); - - PLy_input_datum_func2(&(arg->in.r.atts[i]), arg->mcxt, - attr->atttypid, - typeTup, - exec_ctx->curr_proc->langid, - exec_ctx->curr_proc->trftypes); - - ReleaseSysCache(typeTup); + PLy_input_setup_func(att, arg->mcxt, + attr->atttypid, attr->atttypmod, + proc); } - - MemoryContextSwitchTo(oldcxt); } +/* + * Initialize, or re-initialize, per-column output info for a composite type. + * + * This is separate from PLy_output_setup_func() because in cases involving + * anonymous record types, we need to be passed the tupdesc explicitly. + * It's caller's responsibility that the tupdesc has adequate lifespan + * in such cases. If the tupdesc is for a named composite or registered + * record type, it does not need to be long-lived. + */ void -PLy_output_tuple_funcs(PLyTypeInfo *arg, TupleDesc desc) +PLy_output_setup_tuple(PLyObToDatum *arg, TupleDesc desc, PLyProcedure *proc) { int i; - PLyExecutionContext *exec_ctx = PLy_current_execution_context(); - MemoryContext oldcxt; - - oldcxt = MemoryContextSwitchTo(arg->mcxt); - - if (arg->is_rowtype == 0) - elog(ERROR, "PLyTypeInfo struct is initialized for a Datum"); - arg->is_rowtype = 1; - if (arg->out.r.natts != desc->natts) - { - if (arg->out.r.atts) - pfree(arg->out.r.atts); - arg->out.r.natts = desc->natts; - arg->out.r.atts = palloc0(desc->natts * sizeof(PLyObToDatum)); - } + /* We should be working on a previously-set-up struct */ + Assert(arg->func == PLyObject_ToComposite); - Assert(OidIsValid(desc->tdtypeid)); + /* Save pointer to tupdesc, but only if this is an anonymous record type */ + if (arg->typoid == RECORDOID && arg->typmod < 0) + arg->u.tuple.recdesc = desc; - /* - * RECORDOID means we got called to create output functions for an - * anonymous record type - */ - if (desc->tdtypeid != RECORDOID) + /* (Re)allocate atts array as needed */ + if (arg->u.tuple.natts != desc->natts) { - HeapTuple relTup; - - /* Get the pg_class tuple corresponding to the type of the output */ - arg->typ_relid = typeidTypeRelid(desc->tdtypeid); - relTup = SearchSysCache1(RELOID, ObjectIdGetDatum(arg->typ_relid)); - if (!HeapTupleIsValid(relTup)) - elog(ERROR, "cache lookup failed for relation %u", arg->typ_relid); - - /* Remember XMIN and TID for later validation if cache is still OK */ - arg->typrel_xmin = HeapTupleHeaderGetRawXmin(relTup->t_data); - arg->typrel_tid = relTup->t_self; - - ReleaseSysCache(relTup); + if (arg->u.tuple.atts) + pfree(arg->u.tuple.atts); + arg->u.tuple.natts = desc->natts; + arg->u.tuple.atts = (PLyObToDatum *) + MemoryContextAllocZero(arg->mcxt, + desc->natts * sizeof(PLyObToDatum)); } + /* Fill the atts entries, except for dropped columns */ for (i = 0; i < desc->natts; i++) { - HeapTuple typeTup; Form_pg_attribute attr = TupleDescAttr(desc, i); + PLyObToDatum *att = &arg->u.tuple.atts[i]; if (attr->attisdropped) continue; - if (arg->out.r.atts[i].typoid == attr->atttypid) + if (att->typoid == attr->atttypid && att->typmod == attr->atttypmod) continue; /* already set up this entry */ - typeTup = SearchSysCache1(TYPEOID, ObjectIdGetDatum(attr->atttypid)); - if (!HeapTupleIsValid(typeTup)) - elog(ERROR, "cache lookup failed for type %u", - attr->atttypid); - - PLy_output_datum_func2(&(arg->out.r.atts[i]), arg->mcxt, typeTup, - exec_ctx->curr_proc->langid, - exec_ctx->curr_proc->trftypes); - - ReleaseSysCache(typeTup); + PLy_output_setup_func(att, arg->mcxt, + attr->atttypid, attr->atttypmod, + proc); } - - MemoryContextSwitchTo(oldcxt); } +/* + * Set up output info for a PL/Python function returning record. + * + * Note: the given tupdesc is not necessarily long-lived. + */ void -PLy_output_record_funcs(PLyTypeInfo *arg, TupleDesc desc) +PLy_output_setup_record(PLyObToDatum *arg, TupleDesc desc, PLyProcedure *proc) { + /* Makes no sense unless RECORD */ + Assert(arg->typoid == RECORDOID); + Assert(desc->tdtypeid == RECORDOID); + /* - * If the output record functions are already set, we just have to check - * if the record descriptor has not changed + * Bless the record type if not already done. We'd have to do this anyway + * to return a tuple, so we might as well force the issue so we can use + * the known-record-type code path. */ - if ((arg->is_rowtype == 1) && - (arg->out.d.typmod != -1) && - (arg->out.d.typmod == desc->tdtypmod)) - return; - - /* bless the record to make it known to the typcache lookup code */ BlessTupleDesc(desc); - /* save the freshly generated typmod */ - arg->out.d.typmod = desc->tdtypmod; - /* proceed with normal I/O function caching */ - PLy_output_tuple_funcs(arg, desc); /* - * it should change is_rowtype to 1, so we won't go through this again - * unless the output record description changes + * Update arg->typmod, and clear the recdesc link if it's changed. The + * next call of PLyObject_ToComposite will look up a long-lived tupdesc + * for the record type. */ - Assert(arg->is_rowtype == 1); + arg->typmod = desc->tdtypmod; + if (arg->u.tuple.recdesc && + arg->u.tuple.recdesc->tdtypmod != arg->typmod) + arg->u.tuple.recdesc = NULL; + + /* Update derived data if necessary */ + PLy_output_setup_tuple(arg, desc, proc); } /* - * Transform a tuple into a Python dict object. + * Recursively initialize the PLyObToDatum structure(s) needed to construct + * a SQL value of the specified typeOid/typmod from a Python value. + * (But note that at this point we may have RECORDOID/-1, ie, an indeterminate + * record type.) + * proc is used to look up transform functions. */ -PyObject * -PLyDict_FromTuple(PLyTypeInfo *info, HeapTuple tuple, TupleDesc desc) +void +PLy_output_setup_func(PLyObToDatum *arg, MemoryContext arg_mcxt, + Oid typeOid, int32 typmod, + PLyProcedure *proc) { - PyObject *volatile dict; - PLyExecutionContext *exec_ctx = PLy_current_execution_context(); - MemoryContext scratch_context = PLy_get_scratch_context(exec_ctx); - MemoryContext oldcontext = CurrentMemoryContext; + TypeCacheEntry *typentry; + char typtype; + Oid trfuncid; + Oid typinput; - if (info->is_rowtype != 1) - elog(ERROR, "PLyTypeInfo structure describes a datum"); + /* Since this is recursive, it could theoretically be driven to overflow */ + check_stack_depth(); - dict = PyDict_New(); - if (dict == NULL) - PLy_elog(ERROR, "could not create new dictionary"); + arg->typoid = typeOid; + arg->typmod = typmod; + arg->mcxt = arg_mcxt; - PG_TRY(); + /* + * Fetch typcache entry for the target type, asking for whatever info + * we'll need later. RECORD is a special case: just treat it as composite + * without bothering with the typcache entry. + */ + if (typeOid != RECORDOID) { - int i; - - /* - * Do the work in the scratch context to avoid leaking memory from the - * datatype output function calls. - */ - MemoryContextSwitchTo(scratch_context); - for (i = 0; i < info->in.r.natts; i++) - { - char *key; - Datum vattr; - bool is_null; - PyObject *value; - Form_pg_attribute attr = TupleDescAttr(desc, i); - - if (attr->attisdropped) - continue; - - key = NameStr(attr->attname); - vattr = heap_getattr(tuple, (i + 1), desc, &is_null); - - if (is_null || info->in.r.atts[i].func == NULL) - PyDict_SetItemString(dict, key, Py_None); - else - { - value = (info->in.r.atts[i].func) (&info->in.r.atts[i], vattr); - PyDict_SetItemString(dict, key, value); - Py_DECREF(value); - } - } - MemoryContextSwitchTo(oldcontext); - MemoryContextReset(scratch_context); + typentry = lookup_type_cache(typeOid, TYPECACHE_DOMAIN_BASE_INFO); + typtype = typentry->typtype; + arg->typbyval = typentry->typbyval; + arg->typlen = typentry->typlen; + arg->typalign = typentry->typalign; } - PG_CATCH(); + else { - MemoryContextSwitchTo(oldcontext); - Py_DECREF(dict); - PG_RE_THROW(); + typentry = NULL; + typtype = TYPTYPE_COMPOSITE; + /* hard-wired knowledge about type RECORD: */ + arg->typbyval = false; + arg->typlen = -1; + arg->typalign = 'd'; } - PG_END_TRY(); - - return dict; -} - -/* - * Convert a Python object to a composite Datum, using all supported - * conversion methods: composite as a string, as a sequence, as a mapping or - * as an object that has __getattr__ support. - */ -Datum -PLyObject_ToCompositeDatum(PLyTypeInfo *info, TupleDesc desc, PyObject *plrv, bool inarray) -{ - Datum datum; - - if (PyString_Check(plrv) || PyUnicode_Check(plrv)) - datum = PLyString_ToComposite(info, desc, plrv, inarray); - else if (PySequence_Check(plrv)) - /* composite type as sequence (tuple, list etc) */ - datum = PLySequence_ToComposite(info, desc, plrv); - else if (PyMapping_Check(plrv)) - /* composite type as mapping (currently only dict) */ - datum = PLyMapping_ToComposite(info, desc, plrv); - else - /* returned as smth, must provide method __getattr__(name) */ - datum = PLyGenericObject_ToComposite(info, desc, plrv, inarray); - - return datum; -} - -static void -PLy_output_datum_func2(PLyObToDatum *arg, MemoryContext arg_mcxt, HeapTuple typeTup, Oid langid, List *trftypes) -{ - Form_pg_type typeStruct = (Form_pg_type) GETSTRUCT(typeTup); - Oid element_type; - Oid base_type; - Oid funcid; - MemoryContext oldcxt; - - oldcxt = MemoryContextSwitchTo(arg_mcxt); - - fmgr_info_cxt(typeStruct->typinput, &arg->typfunc, arg_mcxt); - arg->typoid = HeapTupleGetOid(typeTup); - arg->typmod = -1; - arg->typioparam = getTypeIOParam(typeTup); - arg->typbyval = typeStruct->typbyval; - - element_type = get_base_element_type(arg->typoid); - base_type = getBaseType(element_type ? element_type : arg->typoid); /* - * Select a conversion function to convert Python objects to PostgreSQL - * datums. + * Choose conversion method. Note that transform functions are checked + * for composite and scalar types, but not for arrays or domains. This is + * somewhat historical, but we'd have a problem allowing them on domains, + * since we drill down through all levels of a domain nest without looking + * at the intermediate levels at all. */ - - if ((funcid = get_transform_tosql(base_type, langid, trftypes))) + if (typtype == TYPTYPE_DOMAIN) + { + /* Domain */ + arg->func = PLyObject_ToDomain; + arg->u.domain.domain_info = NULL; + /* Recursively set up conversion info for the element type */ + arg->u.domain.base = (PLyObToDatum *) + MemoryContextAllocZero(arg_mcxt, sizeof(PLyObToDatum)); + PLy_output_setup_func(arg->u.domain.base, arg_mcxt, + typentry->domainBaseType, + typentry->domainBaseTypmod, + proc); + } + else if (typentry && + OidIsValid(typentry->typelem) && typentry->typlen == -1) + { + /* Standard varlena array (cf. get_element_type) */ + arg->func = PLySequence_ToArray; + /* Get base type OID to insert into constructed array */ + /* (note this might not be the same as the immediate child type) */ + arg->u.array.elmbasetype = getBaseType(typentry->typelem); + /* Recursively set up conversion info for the element type */ + arg->u.array.elm = (PLyObToDatum *) + MemoryContextAllocZero(arg_mcxt, sizeof(PLyObToDatum)); + PLy_output_setup_func(arg->u.array.elm, arg_mcxt, + typentry->typelem, typmod, + proc); + } + else if ((trfuncid = get_transform_tosql(typeOid, + proc->langid, + proc->trftypes))) { arg->func = PLyObject_ToTransform; - fmgr_info_cxt(funcid, &arg->typtransform, arg_mcxt); + fmgr_info_cxt(trfuncid, &arg->u.transform.typtransform, arg_mcxt); } - else if (typeStruct->typtype == TYPTYPE_COMPOSITE) + else if (typtype == TYPTYPE_COMPOSITE) { + /* Named composite type, or RECORD */ arg->func = PLyObject_ToComposite; + /* We'll set up the per-field data later */ + arg->u.tuple.recdesc = NULL; + arg->u.tuple.typentry = typentry; + arg->u.tuple.tupdescseq = typentry ? typentry->tupDescSeqNo - 1 : 0; + arg->u.tuple.atts = NULL; + arg->u.tuple.natts = 0; + /* Mark this invalid till needed, too */ + arg->u.tuple.recinfunc.fn_oid = InvalidOid; } else - switch (base_type) + { + /* Scalar type, but we have a couple of special cases */ + switch (typeOid) { case BOOLOID: arg->func = PLyObject_ToBool; @@ -406,66 +402,111 @@ PLy_output_datum_func2(PLyObToDatum *arg, MemoryContext arg_mcxt, HeapTuple type arg->func = PLyObject_ToBytea; break; default: - arg->func = PLyObject_ToDatum; + arg->func = PLyObject_ToScalar; + getTypeInputInfo(typeOid, &typinput, &arg->u.scalar.typioparam); + fmgr_info_cxt(typinput, &arg->u.scalar.typfunc, arg_mcxt); break; } - - if (element_type) - { - char dummy_delim; - Oid funcid; - - if (type_is_rowtype(element_type)) - arg->func = PLyObject_ToComposite; - - arg->elm = palloc0(sizeof(*arg->elm)); - arg->elm->func = arg->func; - arg->elm->typtransform = arg->typtransform; - arg->func = PLySequence_ToArray; - - arg->elm->typoid = element_type; - arg->elm->typmod = -1; - get_type_io_data(element_type, IOFunc_input, - &arg->elm->typlen, &arg->elm->typbyval, &arg->elm->typalign, &dummy_delim, - &arg->elm->typioparam, &funcid); - fmgr_info_cxt(funcid, &arg->elm->typfunc, arg_mcxt); } - - MemoryContextSwitchTo(oldcxt); } -static void -PLy_input_datum_func2(PLyDatumToOb *arg, MemoryContext arg_mcxt, Oid typeOid, HeapTuple typeTup, Oid langid, List *trftypes) +/* + * Recursively initialize the PLyDatumToOb structure(s) needed to construct + * a Python value from a SQL value of the specified typeOid/typmod. + * (But note that at this point we may have RECORDOID/-1, ie, an indeterminate + * record type.) + * proc is used to look up transform functions. + */ +void +PLy_input_setup_func(PLyDatumToOb *arg, MemoryContext arg_mcxt, + Oid typeOid, int32 typmod, + PLyProcedure *proc) { - Form_pg_type typeStruct = (Form_pg_type) GETSTRUCT(typeTup); - Oid element_type; - Oid base_type; - Oid funcid; - MemoryContext oldcxt; - - oldcxt = MemoryContextSwitchTo(arg_mcxt); + TypeCacheEntry *typentry; + char typtype; + Oid trfuncid; + Oid typoutput; + bool typisvarlena; - /* Get the type's conversion information */ - fmgr_info_cxt(typeStruct->typoutput, &arg->typfunc, arg_mcxt); - arg->typoid = HeapTupleGetOid(typeTup); - arg->typmod = -1; - arg->typioparam = getTypeIOParam(typeTup); - arg->typbyval = typeStruct->typbyval; - arg->typlen = typeStruct->typlen; - arg->typalign = typeStruct->typalign; + /* Since this is recursive, it could theoretically be driven to overflow */ + check_stack_depth(); - /* Determine which kind of Python object we will convert to */ + arg->typoid = typeOid; + arg->typmod = typmod; + arg->mcxt = arg_mcxt; - element_type = get_base_element_type(typeOid); - base_type = getBaseType(element_type ? element_type : typeOid); + /* + * Fetch typcache entry for the target type, asking for whatever info + * we'll need later. RECORD is a special case: just treat it as composite + * without bothering with the typcache entry. + */ + if (typeOid != RECORDOID) + { + typentry = lookup_type_cache(typeOid, TYPECACHE_DOMAIN_BASE_INFO); + typtype = typentry->typtype; + arg->typbyval = typentry->typbyval; + arg->typlen = typentry->typlen; + arg->typalign = typentry->typalign; + } + else + { + typentry = NULL; + typtype = TYPTYPE_COMPOSITE; + /* hard-wired knowledge about type RECORD: */ + arg->typbyval = false; + arg->typlen = -1; + arg->typalign = 'd'; + } - if ((funcid = get_transform_fromsql(base_type, langid, trftypes))) + /* + * Choose conversion method. Note that transform functions are checked + * for composite and scalar types, but not for arrays or domains. This is + * somewhat historical, but we'd have a problem allowing them on domains, + * since we drill down through all levels of a domain nest without looking + * at the intermediate levels at all. + */ + if (typtype == TYPTYPE_DOMAIN) + { + /* Domain --- we don't care, just recurse down to the base type */ + PLy_input_setup_func(arg, arg_mcxt, + typentry->domainBaseType, + typentry->domainBaseTypmod, + proc); + } + else if (typentry && + OidIsValid(typentry->typelem) && typentry->typlen == -1) + { + /* Standard varlena array (cf. get_element_type) */ + arg->func = PLyList_FromArray; + /* Recursively set up conversion info for the element type */ + arg->u.array.elm = (PLyDatumToOb *) + MemoryContextAllocZero(arg_mcxt, sizeof(PLyDatumToOb)); + PLy_input_setup_func(arg->u.array.elm, arg_mcxt, + typentry->typelem, typmod, + proc); + } + else if ((trfuncid = get_transform_fromsql(typeOid, + proc->langid, + proc->trftypes))) { arg->func = PLyObject_FromTransform; - fmgr_info_cxt(funcid, &arg->typtransform, arg_mcxt); + fmgr_info_cxt(trfuncid, &arg->u.transform.typtransform, arg_mcxt); + } + else if (typtype == TYPTYPE_COMPOSITE) + { + /* Named composite type, or RECORD */ + arg->func = PLyDict_FromComposite; + /* We'll set up the per-field data later */ + arg->u.tuple.recdesc = NULL; + arg->u.tuple.typentry = typentry; + arg->u.tuple.tupdescseq = typentry ? typentry->tupDescSeqNo - 1 : 0; + arg->u.tuple.atts = NULL; + arg->u.tuple.natts = 0; } else - switch (base_type) + { + /* Scalar type, but we have a couple of special cases */ + switch (typeOid) { case BOOLOID: arg->func = PLyBool_FromBool; @@ -495,30 +536,19 @@ PLy_input_datum_func2(PLyDatumToOb *arg, MemoryContext arg_mcxt, Oid typeOid, He arg->func = PLyBytes_FromBytea; break; default: - arg->func = PLyString_FromDatum; + arg->func = PLyString_FromScalar; + getTypeOutputInfo(typeOid, &typoutput, &typisvarlena); + fmgr_info_cxt(typoutput, &arg->u.scalar.typfunc, arg_mcxt); break; } - - if (element_type) - { - char dummy_delim; - Oid funcid; - - arg->elm = palloc0(sizeof(*arg->elm)); - arg->elm->func = arg->func; - arg->elm->typtransform = arg->typtransform; - arg->func = PLyList_FromArray; - arg->elm->typoid = element_type; - arg->elm->typmod = -1; - get_type_io_data(element_type, IOFunc_output, - &arg->elm->typlen, &arg->elm->typbyval, &arg->elm->typalign, &dummy_delim, - &arg->elm->typioparam, &funcid); - fmgr_info_cxt(funcid, &arg->elm->typfunc, arg_mcxt); } - - MemoryContextSwitchTo(oldcxt); } + +/* + * Special-purpose input converters. + */ + static PyObject * PLyBool_FromBool(PLyDatumToOb *arg, Datum d) { @@ -611,27 +641,40 @@ PLyBytes_FromBytea(PLyDatumToOb *arg, Datum d) return PyBytes_FromStringAndSize(str, size); } + +/* + * Generic input conversion using a SQL type's output function. + */ static PyObject * -PLyString_FromDatum(PLyDatumToOb *arg, Datum d) +PLyString_FromScalar(PLyDatumToOb *arg, Datum d) { - char *x = OutputFunctionCall(&arg->typfunc, d); + char *x = OutputFunctionCall(&arg->u.scalar.typfunc, d); PyObject *r = PyString_FromString(x); pfree(x); return r; } +/* + * Convert using a from-SQL transform function. + */ static PyObject * PLyObject_FromTransform(PLyDatumToOb *arg, Datum d) { - return (PyObject *) DatumGetPointer(FunctionCall1(&arg->typtransform, d)); + Datum t; + + t = FunctionCall1(&arg->u.transform.typtransform, d); + return (PyObject *) DatumGetPointer(t); } +/* + * Convert a SQL array to a Python list. + */ static PyObject * PLyList_FromArray(PLyDatumToOb *arg, Datum d) { ArrayType *array = DatumGetArrayTypeP(d); - PLyDatumToOb *elm = arg->elm; + PLyDatumToOb *elm = arg->u.array.elm; int ndim; int *dims; char *dataptr; @@ -737,23 +780,110 @@ PLyList_FromArray_recurse(PLyDatumToOb *elm, int *dims, int ndim, int dim, } /* + * Convert a composite SQL value to a Python dict. + */ +static PyObject * +PLyDict_FromComposite(PLyDatumToOb *arg, Datum d) +{ + PyObject *dict; + HeapTupleHeader td; + Oid tupType; + int32 tupTypmod; + TupleDesc tupdesc; + HeapTupleData tmptup; + + td = DatumGetHeapTupleHeader(d); + /* Extract rowtype info and find a tupdesc */ + tupType = HeapTupleHeaderGetTypeId(td); + tupTypmod = HeapTupleHeaderGetTypMod(td); + tupdesc = lookup_rowtype_tupdesc(tupType, tupTypmod); + + /* Set up I/O funcs if not done yet */ + PLy_input_setup_tuple(arg, tupdesc, + PLy_current_execution_context()->curr_proc); + + /* Build a temporary HeapTuple control structure */ + tmptup.t_len = HeapTupleHeaderGetDatumLength(td); + tmptup.t_data = td; + + dict = PLyDict_FromTuple(arg, &tmptup, tupdesc); + + ReleaseTupleDesc(tupdesc); + + return dict; +} + +/* + * Transform a tuple into a Python dict object. + */ +static PyObject * +PLyDict_FromTuple(PLyDatumToOb *arg, HeapTuple tuple, TupleDesc desc) +{ + PyObject *volatile dict; + + /* Simple sanity check that desc matches */ + Assert(desc->natts == arg->u.tuple.natts); + + dict = PyDict_New(); + if (dict == NULL) + PLy_elog(ERROR, "could not create new dictionary"); + + PG_TRY(); + { + int i; + + for (i = 0; i < arg->u.tuple.natts; i++) + { + PLyDatumToOb *att = &arg->u.tuple.atts[i]; + Form_pg_attribute attr = TupleDescAttr(desc, i); + char *key; + Datum vattr; + bool is_null; + PyObject *value; + + if (attr->attisdropped) + continue; + + key = NameStr(attr->attname); + vattr = heap_getattr(tuple, (i + 1), desc, &is_null); + + if (is_null) + PyDict_SetItemString(dict, key, Py_None); + else + { + value = att->func(att, vattr); + PyDict_SetItemString(dict, key, value); + Py_DECREF(value); + } + } + } + PG_CATCH(); + { + Py_DECREF(dict); + PG_RE_THROW(); + } + PG_END_TRY(); + + return dict; +} + +/* * Convert a Python object to a PostgreSQL bool datum. This can't go * through the generic conversion function, because Python attaches a * Boolean value to everything, more things than the PostgreSQL bool * type can parse. */ static Datum -PLyObject_ToBool(PLyObToDatum *arg, int32 typmod, PyObject *plrv, bool inarray) +PLyObject_ToBool(PLyObToDatum *arg, PyObject *plrv, + bool *isnull, bool inarray) { - Datum rv; - - Assert(plrv != Py_None); - rv = BoolGetDatum(PyObject_IsTrue(plrv)); - - if (get_typtype(arg->typoid) == TYPTYPE_DOMAIN) - domain_check(rv, false, arg->typoid, &arg->typfunc.fn_extra, arg->typfunc.fn_mcxt); - - return rv; + if (plrv == Py_None) + { + *isnull = true; + return (Datum) 0; + } + *isnull = false; + return BoolGetDatum(PyObject_IsTrue(plrv)); } /* @@ -762,12 +892,18 @@ PLyObject_ToBool(PLyObToDatum *arg, int32 typmod, PyObject *plrv, bool inarray) * with embedded nulls. And it's faster this way. */ static Datum -PLyObject_ToBytea(PLyObToDatum *arg, int32 typmod, PyObject *plrv, bool inarray) +PLyObject_ToBytea(PLyObToDatum *arg, PyObject *plrv, + bool *isnull, bool inarray) { PyObject *volatile plrv_so = NULL; Datum rv; - Assert(plrv != Py_None); + if (plrv == Py_None) + { + *isnull = true; + return (Datum) 0; + } + *isnull = false; plrv_so = PyObject_Bytes(plrv); if (!plrv_so) @@ -793,9 +929,6 @@ PLyObject_ToBytea(PLyObToDatum *arg, int32 typmod, PyObject *plrv, bool inarray) Py_XDECREF(plrv_so); - if (get_typtype(arg->typoid) == TYPTYPE_DOMAIN) - domain_check(rv, false, arg->typoid, &arg->typfunc.fn_extra, arg->typfunc.fn_mcxt); - return rv; } @@ -806,45 +939,87 @@ PLyObject_ToBytea(PLyObToDatum *arg, int32 typmod, PyObject *plrv, bool inarray) * for obtaining PostgreSQL tuples. */ static Datum -PLyObject_ToComposite(PLyObToDatum *arg, int32 typmod, PyObject *plrv, bool inarray) +PLyObject_ToComposite(PLyObToDatum *arg, PyObject *plrv, + bool *isnull, bool inarray) { Datum rv; - PLyTypeInfo info; TupleDesc desc; - MemoryContext cxt; - if (typmod != -1) - elog(ERROR, "received unnamed record type as input"); + if (plrv == Py_None) + { + *isnull = true; + return (Datum) 0; + } + *isnull = false; + + /* + * The string conversion case doesn't require a tupdesc, nor per-field + * conversion data, so just go for it if that's the case to use. + */ + if (PyString_Check(plrv) || PyUnicode_Check(plrv)) + return PLyString_ToComposite(arg, plrv, inarray); - /* Create a dummy PLyTypeInfo */ - cxt = AllocSetContextCreate(CurrentMemoryContext, - "PL/Python temp context", - ALLOCSET_DEFAULT_SIZES); - MemSet(&info, 0, sizeof(PLyTypeInfo)); - PLy_typeinfo_init(&info, cxt); - /* Mark it as needing output routines lookup */ - info.is_rowtype = 2; + /* + * If we're dealing with a named composite type, we must look up the + * tupdesc every time, to protect against possible changes to the type. + * RECORD types can't change between calls; but we must still be willing + * to set up the info the first time, if nobody did yet. + */ + if (arg->typoid != RECORDOID) + { + desc = lookup_rowtype_tupdesc(arg->typoid, arg->typmod); + /* We should have the descriptor of the type's typcache entry */ + Assert(desc == arg->u.tuple.typentry->tupDesc); + /* Detect change of descriptor, update cache if needed */ + if (arg->u.tuple.tupdescseq != arg->u.tuple.typentry->tupDescSeqNo) + { + PLy_output_setup_tuple(arg, desc, + PLy_current_execution_context()->curr_proc); + arg->u.tuple.tupdescseq = arg->u.tuple.typentry->tupDescSeqNo; + } + } + else + { + desc = arg->u.tuple.recdesc; + if (desc == NULL) + { + desc = lookup_rowtype_tupdesc(arg->typoid, arg->typmod); + arg->u.tuple.recdesc = desc; + } + else + { + /* Pin descriptor to match unpin below */ + PinTupleDesc(desc); + } + } - desc = lookup_rowtype_tupdesc(arg->typoid, arg->typmod); + /* Simple sanity check on our caching */ + Assert(desc->natts == arg->u.tuple.natts); /* - * This will set up the dummy PLyTypeInfo's output conversion routines, - * since we left is_rowtype as 2. A future optimization could be caching - * that info instead of looking it up every time a tuple is returned from - * the function. + * Convert, using the appropriate method depending on the type of the + * supplied Python object. */ - rv = PLyObject_ToCompositeDatum(&info, desc, plrv, inarray); + if (PySequence_Check(plrv)) + /* composite type as sequence (tuple, list etc) */ + rv = PLySequence_ToComposite(arg, desc, plrv); + else if (PyMapping_Check(plrv)) + /* composite type as mapping (currently only dict) */ + rv = PLyMapping_ToComposite(arg, desc, plrv); + else + /* returned as smth, must provide method __getattr__(name) */ + rv = PLyGenericObject_ToComposite(arg, desc, plrv, inarray); ReleaseTupleDesc(desc); - MemoryContextDelete(cxt); - return rv; } /* * Convert Python object to C string in server encoding. + * + * Note: this is exported for use by add-on transform modules. */ char * PLyObject_AsString(PyObject *plrv) @@ -901,74 +1076,71 @@ PLyObject_AsString(PyObject *plrv) /* - * Generic conversion function: Convert PyObject to cstring and + * Generic output conversion function: convert PyObject to cstring and * cstring into PostgreSQL type. */ static Datum -PLyObject_ToDatum(PLyObToDatum *arg, int32 typmod, PyObject *plrv, bool inarray) +PLyObject_ToScalar(PLyObToDatum *arg, PyObject *plrv, + bool *isnull, bool inarray) { char *str; - Assert(plrv != Py_None); + if (plrv == Py_None) + { + *isnull = true; + return (Datum) 0; + } + *isnull = false; str = PLyObject_AsString(plrv); - /* - * If we are parsing a composite type within an array, and the string - * isn't a valid record literal, there's a high chance that the function - * did something like: - * - * CREATE FUNCTION .. RETURNS comptype[] AS $$ return [['foo', 'bar']] $$ - * LANGUAGE plpython; - * - * Before PostgreSQL 10, that was interpreted as a single-dimensional - * array, containing record ('foo', 'bar'). PostgreSQL 10 added support - * for multi-dimensional arrays, and it is now interpreted as a - * two-dimensional array, containing two records, 'foo', and 'bar'. - * record_in() will throw an error, because "foo" is not a valid record - * literal. - * - * To make that less confusing to users who are upgrading from older - * versions, try to give a hint in the typical instances of that. If we - * are parsing an array of composite types, and we see a string literal - * that is not a valid record literal, give a hint. We only want to give - * the hint in the narrow case of a malformed string literal, not any - * error from record_in(), so check for that case here specifically. - * - * This check better match the one in record_in(), so that we don't forbid - * literals that are actually valid! - */ - if (inarray && arg->typfunc.fn_oid == F_RECORD_IN) - { - char *ptr = str; + return InputFunctionCall(&arg->u.scalar.typfunc, + str, + arg->u.scalar.typioparam, + arg->typmod); +} - /* Allow leading whitespace */ - while (*ptr && isspace((unsigned char) *ptr)) - ptr++; - if (*ptr++ != '(') - ereport(ERROR, - (errcode(ERRCODE_INVALID_TEXT_REPRESENTATION), - errmsg("malformed record literal: \"%s\"", str), - errdetail("Missing left parenthesis."), - errhint("To return a composite type in an array, return the composite type as a Python tuple, e.g., \"[('foo',)]\"."))); - } - return InputFunctionCall(&arg->typfunc, - str, - arg->typioparam, - typmod); +/* + * Convert to a domain type. + */ +static Datum +PLyObject_ToDomain(PLyObToDatum *arg, PyObject *plrv, + bool *isnull, bool inarray) +{ + Datum result; + PLyObToDatum *base = arg->u.domain.base; + + result = base->func(base, plrv, isnull, inarray); + domain_check(result, *isnull, arg->typoid, + &arg->u.domain.domain_info, arg->mcxt); + return result; } +/* + * Convert using a to-SQL transform function. + */ static Datum -PLyObject_ToTransform(PLyObToDatum *arg, int32 typmod, PyObject *plrv, bool inarray) +PLyObject_ToTransform(PLyObToDatum *arg, PyObject *plrv, + bool *isnull, bool inarray) { - return FunctionCall1(&arg->typtransform, PointerGetDatum(plrv)); + if (plrv == Py_None) + { + *isnull = true; + return (Datum) 0; + } + *isnull = false; + return FunctionCall1(&arg->u.transform.typtransform, PointerGetDatum(plrv)); } +/* + * Convert Python sequence to SQL array. + */ static Datum -PLySequence_ToArray(PLyObToDatum *arg, int32 typmod, PyObject *plrv, bool inarray) +PLySequence_ToArray(PLyObToDatum *arg, PyObject *plrv, + bool *isnull, bool inarray) { ArrayType *array; int i; @@ -979,11 +1151,15 @@ PLySequence_ToArray(PLyObToDatum *arg, int32 typmod, PyObject *plrv, bool inarra int dims[MAXDIM]; int lbs[MAXDIM]; int currelem; - Datum rv; PyObject *pyptr = plrv; PyObject *next; - Assert(plrv != Py_None); + if (plrv == Py_None) + { + *isnull = true; + return (Datum) 0; + } + *isnull = false; /* * Determine the number of dimensions, and their sizes. @@ -1049,7 +1225,7 @@ PLySequence_ToArray(PLyObToDatum *arg, int32 typmod, PyObject *plrv, bool inarra elems = palloc(sizeof(Datum) * len); nulls = palloc(sizeof(bool) * len); currelem = 0; - PLySequence_ToArray_recurse(arg->elm, plrv, + PLySequence_ToArray_recurse(arg->u.array.elm, plrv, dims, ndim, 0, elems, nulls, &currelem); @@ -1061,19 +1237,12 @@ PLySequence_ToArray(PLyObToDatum *arg, int32 typmod, PyObject *plrv, bool inarra ndim, dims, lbs, - get_base_element_type(arg->typoid), - arg->elm->typlen, - arg->elm->typbyval, - arg->elm->typalign); + arg->u.array.elmbasetype, + arg->u.array.elm->typlen, + arg->u.array.elm->typbyval, + arg->u.array.elm->typalign); - /* - * If the result type is a domain of array, the resulting array must be - * checked. - */ - rv = PointerGetDatum(array); - if (get_typtype(arg->typoid) == TYPTYPE_DOMAIN) - domain_check(rv, false, arg->typoid, &arg->typfunc.fn_extra, arg->typfunc.fn_mcxt); - return rv; + return PointerGetDatum(array); } /* @@ -1110,16 +1279,7 @@ PLySequence_ToArray_recurse(PLyObToDatum *elm, PyObject *list, { PyObject *obj = PySequence_GetItem(list, i); - if (obj == Py_None) - { - nulls[*currelem] = true; - elems[*currelem] = (Datum) 0; - } - else - { - nulls[*currelem] = false; - elems[*currelem] = elm->func(elm, -1, obj, true); - } + elems[*currelem] = elm->func(elm, obj, &nulls[*currelem], true); Py_XDECREF(obj); (*currelem)++; } @@ -1127,42 +1287,72 @@ PLySequence_ToArray_recurse(PLyObToDatum *elm, PyObject *list, } +/* + * Convert a Python string to composite, using record_in. + */ static Datum -PLyString_ToComposite(PLyTypeInfo *info, TupleDesc desc, PyObject *string, bool inarray) +PLyString_ToComposite(PLyObToDatum *arg, PyObject *string, bool inarray) { - Datum result; - HeapTuple typeTup; - PLyTypeInfo locinfo; - PLyExecutionContext *exec_ctx = PLy_current_execution_context(); - MemoryContext cxt; - - /* Create a dummy PLyTypeInfo */ - cxt = AllocSetContextCreate(CurrentMemoryContext, - "PL/Python temp context", - ALLOCSET_DEFAULT_SIZES); - MemSet(&locinfo, 0, sizeof(PLyTypeInfo)); - PLy_typeinfo_init(&locinfo, cxt); - - typeTup = SearchSysCache1(TYPEOID, ObjectIdGetDatum(desc->tdtypeid)); - if (!HeapTupleIsValid(typeTup)) - elog(ERROR, "cache lookup failed for type %u", desc->tdtypeid); + char *str; - PLy_output_datum_func2(&locinfo.out.d, locinfo.mcxt, typeTup, - exec_ctx->curr_proc->langid, - exec_ctx->curr_proc->trftypes); + /* + * Set up call data for record_in, if we didn't already. (We can't just + * use DirectFunctionCall, because record_in needs a fn_extra field.) + */ + if (!OidIsValid(arg->u.tuple.recinfunc.fn_oid)) + fmgr_info_cxt(F_RECORD_IN, &arg->u.tuple.recinfunc, arg->mcxt); - ReleaseSysCache(typeTup); + str = PLyObject_AsString(string); - result = PLyObject_ToDatum(&locinfo.out.d, desc->tdtypmod, string, inarray); + /* + * If we are parsing a composite type within an array, and the string + * isn't a valid record literal, there's a high chance that the function + * did something like: + * + * CREATE FUNCTION .. RETURNS comptype[] AS $$ return [['foo', 'bar']] $$ + * LANGUAGE plpython; + * + * Before PostgreSQL 10, that was interpreted as a single-dimensional + * array, containing record ('foo', 'bar'). PostgreSQL 10 added support + * for multi-dimensional arrays, and it is now interpreted as a + * two-dimensional array, containing two records, 'foo', and 'bar'. + * record_in() will throw an error, because "foo" is not a valid record + * literal. + * + * To make that less confusing to users who are upgrading from older + * versions, try to give a hint in the typical instances of that. If we + * are parsing an array of composite types, and we see a string literal + * that is not a valid record literal, give a hint. We only want to give + * the hint in the narrow case of a malformed string literal, not any + * error from record_in(), so check for that case here specifically. + * + * This check better match the one in record_in(), so that we don't forbid + * literals that are actually valid! + */ + if (inarray) + { + char *ptr = str; - MemoryContextDelete(cxt); + /* Allow leading whitespace */ + while (*ptr && isspace((unsigned char) *ptr)) + ptr++; + if (*ptr++ != '(') + ereport(ERROR, + (errcode(ERRCODE_INVALID_TEXT_REPRESENTATION), + errmsg("malformed record literal: \"%s\"", str), + errdetail("Missing left parenthesis."), + errhint("To return a composite type in an array, return the composite type as a Python tuple, e.g., \"[('foo',)]\"."))); + } - return result; + return InputFunctionCall(&arg->u.tuple.recinfunc, + str, + arg->typoid, + arg->typmod); } static Datum -PLyMapping_ToComposite(PLyTypeInfo *info, TupleDesc desc, PyObject *mapping) +PLyMapping_ToComposite(PLyObToDatum *arg, TupleDesc desc, PyObject *mapping) { Datum result; HeapTuple tuple; @@ -1172,10 +1362,6 @@ PLyMapping_ToComposite(PLyTypeInfo *info, TupleDesc desc, PyObject *mapping) Assert(PyMapping_Check(mapping)); - if (info->is_rowtype == 2) - PLy_output_tuple_funcs(info, desc); - Assert(info->is_rowtype == 1); - /* Build tuple */ values = palloc(sizeof(Datum) * desc->natts); nulls = palloc(sizeof(bool) * desc->natts); @@ -1195,27 +1381,19 @@ PLyMapping_ToComposite(PLyTypeInfo *info, TupleDesc desc, PyObject *mapping) key = NameStr(attr->attname); value = NULL; - att = &info->out.r.atts[i]; + att = &arg->u.tuple.atts[i]; PG_TRY(); { value = PyMapping_GetItemString(mapping, key); - if (value == Py_None) - { - values[i] = (Datum) NULL; - nulls[i] = true; - } - else if (value) - { - values[i] = (att->func) (att, -1, value, false); - nulls[i] = false; - } - else + if (!value) ereport(ERROR, (errcode(ERRCODE_UNDEFINED_COLUMN), errmsg("key \"%s\" not found in mapping", key), errhint("To return null in a column, " "add the value None to the mapping with the key named after the column."))); + values[i] = att->func(att, value, &nulls[i], false); + Py_XDECREF(value); value = NULL; } @@ -1239,7 +1417,7 @@ PLyMapping_ToComposite(PLyTypeInfo *info, TupleDesc desc, PyObject *mapping) static Datum -PLySequence_ToComposite(PLyTypeInfo *info, TupleDesc desc, PyObject *sequence) +PLySequence_ToComposite(PLyObToDatum *arg, TupleDesc desc, PyObject *sequence) { Datum result; HeapTuple tuple; @@ -1266,10 +1444,6 @@ PLySequence_ToComposite(PLyTypeInfo *info, TupleDesc desc, PyObject *sequence) (errcode(ERRCODE_DATATYPE_MISMATCH), errmsg("length of returned sequence did not match number of columns in row"))); - if (info->is_rowtype == 2) - PLy_output_tuple_funcs(info, desc); - Assert(info->is_rowtype == 1); - /* Build tuple */ values = palloc(sizeof(Datum) * desc->natts); nulls = palloc(sizeof(bool) * desc->natts); @@ -1287,21 +1461,13 @@ PLySequence_ToComposite(PLyTypeInfo *info, TupleDesc desc, PyObject *sequence) } value = NULL; - att = &info->out.r.atts[i]; + att = &arg->u.tuple.atts[i]; PG_TRY(); { value = PySequence_GetItem(sequence, idx); Assert(value); - if (value == Py_None) - { - values[i] = (Datum) NULL; - nulls[i] = true; - } - else if (value) - { - values[i] = (att->func) (att, -1, value, false); - nulls[i] = false; - } + + values[i] = att->func(att, value, &nulls[i], false); Py_XDECREF(value); value = NULL; @@ -1328,7 +1494,7 @@ PLySequence_ToComposite(PLyTypeInfo *info, TupleDesc desc, PyObject *sequence) static Datum -PLyGenericObject_ToComposite(PLyTypeInfo *info, TupleDesc desc, PyObject *object, bool inarray) +PLyGenericObject_ToComposite(PLyObToDatum *arg, TupleDesc desc, PyObject *object, bool inarray) { Datum result; HeapTuple tuple; @@ -1336,10 +1502,6 @@ PLyGenericObject_ToComposite(PLyTypeInfo *info, TupleDesc desc, PyObject *object bool *nulls; volatile int i; - if (info->is_rowtype == 2) - PLy_output_tuple_funcs(info, desc); - Assert(info->is_rowtype == 1); - /* Build tuple */ values = palloc(sizeof(Datum) * desc->natts); nulls = palloc(sizeof(bool) * desc->natts); @@ -1359,21 +1521,11 @@ PLyGenericObject_ToComposite(PLyTypeInfo *info, TupleDesc desc, PyObject *object key = NameStr(attr->attname); value = NULL; - att = &info->out.r.atts[i]; + att = &arg->u.tuple.atts[i]; PG_TRY(); { value = PyObject_GetAttrString(object, key); - if (value == Py_None) - { - values[i] = (Datum) NULL; - nulls[i] = true; - } - else if (value) - { - values[i] = (att->func) (att, -1, value, false); - nulls[i] = false; - } - else + if (!value) { /* * No attribute for this column in the object. @@ -1384,7 +1536,7 @@ PLyGenericObject_ToComposite(PLyTypeInfo *info, TupleDesc desc, PyObject *object * array, with a composite type (123, 'foo') in it. But now * it's interpreted as a two-dimensional array, and we try to * interpret "123" as the composite type. See also similar - * heuristic in PLyObject_ToDatum(). + * heuristic in PLyObject_ToScalar(). */ ereport(ERROR, (errcode(ERRCODE_UNDEFINED_COLUMN), @@ -1394,6 +1546,8 @@ PLyGenericObject_ToComposite(PLyTypeInfo *info, TupleDesc desc, PyObject *object errhint("To return null in a column, let the returned object have an attribute named after column with value None."))); } + values[i] = att->func(att, value, &nulls[i], false); + Py_XDECREF(value); value = NULL; } diff --git a/src/pl/plpython/plpy_typeio.h b/src/pl/plpython/plpy_typeio.h index 95f84d83418..91870c91b02 100644 --- a/src/pl/plpython/plpy_typeio.h +++ b/src/pl/plpython/plpy_typeio.h @@ -6,117 +6,169 @@ #define PLPY_TYPEIO_H #include "access/htup.h" -#include "access/tupdesc.h" #include "fmgr.h" -#include "storage/itemptr.h" +#include "utils/typcache.h" + +struct PLyProcedure; /* avoid requiring plpy_procedure.h here */ + /* - * Conversion from PostgreSQL Datum to a Python object. + * "Input" conversion from PostgreSQL Datum to a Python object. + * + * arg is the previously-set-up conversion data, val is the value to convert. + * val mustn't be NULL. + * + * Note: the conversion data structs should be regarded as private to + * plpy_typeio.c. We declare them here only so that other modules can + * define structs containing them. */ -struct PLyDatumToOb; -typedef PyObject *(*PLyDatumToObFunc) (struct PLyDatumToOb *arg, Datum val); +typedef struct PLyDatumToOb PLyDatumToOb; /* forward reference */ -typedef struct PLyDatumToOb +typedef PyObject *(*PLyDatumToObFunc) (PLyDatumToOb *arg, Datum val); + +typedef struct PLyScalarToOb { - PLyDatumToObFunc func; - FmgrInfo typfunc; /* The type's output function */ - FmgrInfo typtransform; /* from-SQL transform */ - Oid typoid; /* The OID of the type */ - int32 typmod; /* The typmod of the type */ - Oid typioparam; - bool typbyval; - int16 typlen; - char typalign; - struct PLyDatumToOb *elm; -} PLyDatumToOb; + FmgrInfo typfunc; /* lookup info for type's output function */ +} PLyScalarToOb; + +typedef struct PLyArrayToOb +{ + PLyDatumToOb *elm; /* conversion info for array's element type */ +} PLyArrayToOb; typedef struct PLyTupleToOb { - PLyDatumToOb *atts; - int natts; + /* If we're dealing with a RECORD type, actual descriptor is here: */ + TupleDesc recdesc; + /* If we're dealing with a named composite type, these fields are set: */ + TypeCacheEntry *typentry; /* typcache entry for type */ + int64 tupdescseq; /* last tupdesc seqno seen in typcache */ + /* These fields are NULL/0 if not yet set: */ + PLyDatumToOb *atts; /* array of per-column conversion info */ + int natts; /* length of array */ } PLyTupleToOb; -typedef union PLyTypeInput +typedef struct PLyTransformToOb +{ + FmgrInfo typtransform; /* lookup info for from-SQL transform func */ +} PLyTransformToOb; + +struct PLyDatumToOb { - PLyDatumToOb d; - PLyTupleToOb r; -} PLyTypeInput; + PLyDatumToObFunc func; /* conversion control function */ + Oid typoid; /* OID of the source type */ + int32 typmod; /* typmod of the source type */ + bool typbyval; /* its physical representation details */ + int16 typlen; + char typalign; + MemoryContext mcxt; /* context this info is stored in */ + union /* conversion-type-specific data */ + { + PLyScalarToOb scalar; + PLyArrayToOb array; + PLyTupleToOb tuple; + PLyTransformToOb transform; + } u; +}; /* - * Conversion from Python object to a PostgreSQL Datum. + * "Output" conversion from Python object to a PostgreSQL Datum. + * + * arg is the previously-set-up conversion data, val is the value to convert. * - * The 'inarray' argument to the conversion function is true, if the - * converted value was in an array (Python list). It is used to give a - * better error message in some cases. + * *isnull is set to true if val is Py_None, false otherwise. + * (The conversion function *must* be called even for Py_None, + * so that domain constraints can be checked.) + * + * inarray is true if the converted value was in an array (Python list). + * It is used to give a better error message in some cases. */ -struct PLyObToDatum; -typedef Datum (*PLyObToDatumFunc) (struct PLyObToDatum *arg, int32 typmod, PyObject *val, bool inarray); +typedef struct PLyObToDatum PLyObToDatum; /* forward reference */ + +typedef Datum (*PLyObToDatumFunc) (PLyObToDatum *arg, PyObject *val, + bool *isnull, + bool inarray); -typedef struct PLyObToDatum +typedef struct PLyObToScalar { - PLyObToDatumFunc func; - FmgrInfo typfunc; /* The type's input function */ - FmgrInfo typtransform; /* to-SQL transform */ - Oid typoid; /* The OID of the type */ - int32 typmod; /* The typmod of the type */ - Oid typioparam; - bool typbyval; - int16 typlen; - char typalign; - struct PLyObToDatum *elm; -} PLyObToDatum; + FmgrInfo typfunc; /* lookup info for type's input function */ + Oid typioparam; /* argument to pass to it */ +} PLyObToScalar; + +typedef struct PLyObToArray +{ + PLyObToDatum *elm; /* conversion info for array's element type */ + Oid elmbasetype; /* element base type */ +} PLyObToArray; typedef struct PLyObToTuple { - PLyObToDatum *atts; - int natts; + /* If we're dealing with a RECORD type, actual descriptor is here: */ + TupleDesc recdesc; + /* If we're dealing with a named composite type, these fields are set: */ + TypeCacheEntry *typentry; /* typcache entry for type */ + int64 tupdescseq; /* last tupdesc seqno seen in typcache */ + /* These fields are NULL/0 if not yet set: */ + PLyObToDatum *atts; /* array of per-column conversion info */ + int natts; /* length of array */ + /* We might need to convert using record_in(); if so, cache info here */ + FmgrInfo recinfunc; /* lookup info for record_in */ } PLyObToTuple; -typedef union PLyTypeOutput +typedef struct PLyObToDomain { - PLyObToDatum d; - PLyObToTuple r; -} PLyTypeOutput; + PLyObToDatum *base; /* conversion info for domain's base type */ + void *domain_info; /* cache space for domain_check() */ +} PLyObToDomain; -/* all we need to move PostgreSQL data to Python objects, - * and vice versa - */ -typedef struct PLyTypeInfo +typedef struct PLyObToTransform { - PLyTypeInput in; - PLyTypeOutput out; - - /* - * is_rowtype can be: -1 = not known yet (initial state); 0 = scalar - * datatype; 1 = rowtype; 2 = rowtype, but I/O functions not set up yet - */ - int is_rowtype; - /* used to check if the type has been modified */ - Oid typ_relid; - TransactionId typrel_xmin; - ItemPointerData typrel_tid; - - /* context for subsidiary data (doesn't belong to this struct though) */ - MemoryContext mcxt; -} PLyTypeInfo; - -extern void PLy_typeinfo_init(PLyTypeInfo *arg, MemoryContext mcxt); + FmgrInfo typtransform; /* lookup info for to-SQL transform function */ +} PLyObToTransform; -extern void PLy_input_datum_func(PLyTypeInfo *arg, Oid typeOid, HeapTuple typeTup, Oid langid, List *trftypes); -extern void PLy_output_datum_func(PLyTypeInfo *arg, HeapTuple typeTup, Oid langid, List *trftypes); - -extern void PLy_input_tuple_funcs(PLyTypeInfo *arg, TupleDesc desc); -extern void PLy_output_tuple_funcs(PLyTypeInfo *arg, TupleDesc desc); - -extern void PLy_output_record_funcs(PLyTypeInfo *arg, TupleDesc desc); - -/* conversion from Python objects to composite Datums */ -extern Datum PLyObject_ToCompositeDatum(PLyTypeInfo *info, TupleDesc desc, PyObject *plrv, bool isarray); - -/* conversion from heap tuples to Python dictionaries */ -extern PyObject *PLyDict_FromTuple(PLyTypeInfo *info, HeapTuple tuple, TupleDesc desc); - -/* conversion from Python objects to C strings */ +struct PLyObToDatum +{ + PLyObToDatumFunc func; /* conversion control function */ + Oid typoid; /* OID of the target type */ + int32 typmod; /* typmod of the target type */ + bool typbyval; /* its physical representation details */ + int16 typlen; + char typalign; + MemoryContext mcxt; /* context this info is stored in */ + union /* conversion-type-specific data */ + { + PLyObToScalar scalar; + PLyObToArray array; + PLyObToTuple tuple; + PLyObToDomain domain; + PLyObToTransform transform; + } u; +}; + + +extern PyObject *PLy_input_convert(PLyDatumToOb *arg, Datum val); +extern Datum PLy_output_convert(PLyObToDatum *arg, PyObject *val, + bool *isnull); + +extern PyObject *PLy_input_from_tuple(PLyDatumToOb *arg, HeapTuple tuple, + TupleDesc desc); + +extern void PLy_input_setup_func(PLyDatumToOb *arg, MemoryContext arg_mcxt, + Oid typeOid, int32 typmod, + struct PLyProcedure *proc); +extern void PLy_output_setup_func(PLyObToDatum *arg, MemoryContext arg_mcxt, + Oid typeOid, int32 typmod, + struct PLyProcedure *proc); + +extern void PLy_input_setup_tuple(PLyDatumToOb *arg, TupleDesc desc, + struct PLyProcedure *proc); +extern void PLy_output_setup_tuple(PLyObToDatum *arg, TupleDesc desc, + struct PLyProcedure *proc); + +extern void PLy_output_setup_record(PLyObToDatum *arg, TupleDesc desc, + struct PLyProcedure *proc); + +/* conversion from Python objects to C strings --- exported for transforms */ extern char *PLyObject_AsString(PyObject *plrv); #endif /* PLPY_TYPEIO_H */ diff --git a/src/pl/plpython/sql/plpython_types.sql b/src/pl/plpython/sql/plpython_types.sql index 8c57297c24e..cc0524ee806 100644 --- a/src/pl/plpython/sql/plpython_types.sql +++ b/src/pl/plpython/sql/plpython_types.sql @@ -387,6 +387,55 @@ $$ LANGUAGE plpythonu; SELECT * FROM test_type_conversion_array_domain_check_violation(); +-- +-- Arrays of domains +-- + +CREATE FUNCTION test_read_uint2_array(x uint2[]) RETURNS uint2 AS $$ +plpy.info(x, type(x)) +return x[0] +$$ LANGUAGE plpythonu; + +select test_read_uint2_array(array[1::uint2]); + +CREATE FUNCTION test_build_uint2_array(x int2) RETURNS uint2[] AS $$ +return [x, x] +$$ LANGUAGE plpythonu; + +select test_build_uint2_array(1::int2); +select test_build_uint2_array(-1::int2); -- fail + +-- +-- ideally this would work, but for now it doesn't, because the return value +-- is [[2,4], [2,4]] which our conversion code thinks should become a 2-D +-- integer array, not an array of arrays. +-- +CREATE FUNCTION test_type_conversion_domain_array(x integer[]) + RETURNS ordered_pair_domain[] AS $$ +return [x, x] +$$ LANGUAGE plpythonu; + +select test_type_conversion_domain_array(array[2,4]); +select test_type_conversion_domain_array(array[4,2]); -- fail + +CREATE FUNCTION test_type_conversion_domain_array2(x ordered_pair_domain) + RETURNS integer AS $$ +plpy.info(x, type(x)) +return x[1] +$$ LANGUAGE plpythonu; + +select test_type_conversion_domain_array2(array[2,4]); +select test_type_conversion_domain_array2(array[4,2]); -- fail + +CREATE FUNCTION test_type_conversion_array_domain_array(x ordered_pair_domain[]) + RETURNS ordered_pair_domain AS $$ +plpy.info(x, type(x)) +return x[0] +$$ LANGUAGE plpythonu; + +select test_type_conversion_array_domain_array(array[array[2,4]::ordered_pair_domain]); + + --- --- Composite types --- @@ -431,6 +480,48 @@ SELECT test_composite_type_input(row(1, 2)); -- +-- Domains within composite +-- + +CREATE TYPE nnint_container AS (f1 int, f2 nnint); + +CREATE FUNCTION nnint_test(x int, y int) RETURNS nnint_container AS $$ +return {'f1': x, 'f2': y} +$$ LANGUAGE plpythonu; + +SELECT nnint_test(null, 3); +SELECT nnint_test(3, null); -- fail + + +-- +-- Domains of composite +-- + +CREATE DOMAIN ordered_named_pair AS named_pair_2 CHECK((VALUE).i <= (VALUE).j); + +CREATE FUNCTION read_ordered_named_pair(p ordered_named_pair) RETURNS integer AS $$ +return p['i'] + p['j'] +$$ LANGUAGE plpythonu; + +SELECT read_ordered_named_pair(row(1, 2)); +SELECT read_ordered_named_pair(row(2, 1)); -- fail + +CREATE FUNCTION build_ordered_named_pair(i int, j int) RETURNS ordered_named_pair AS $$ +return {'i': i, 'j': j} +$$ LANGUAGE plpythonu; + +SELECT build_ordered_named_pair(1,2); +SELECT build_ordered_named_pair(2,1); -- fail + +CREATE FUNCTION build_ordered_named_pairs(i int, j int) RETURNS ordered_named_pair[] AS $$ +return [{'i': i, 'j': j}, {'i': i, 'j': j+1}] +$$ LANGUAGE plpythonu; + +SELECT build_ordered_named_pairs(1,2); +SELECT build_ordered_named_pairs(2,1); -- fail + + +-- -- Prepared statements -- |