From 687f096ea9da82d267f1809a5f3fdfa027092045 Mon Sep 17 00:00:00 2001 From: Tom Lane Date: Thu, 16 Nov 2017 16:22:57 -0500 Subject: [PATCH] Make PL/Python handle domain-type conversions correctly. Fix PL/Python so that it can handle domains over composite, and so that it enforces domain constraints correctly in other cases that were not always done properly before. Notably, it didn't do arrays of domains right (oversight in commit c12d570fa), and it failed to enforce domain constraints when returning a composite type containing a domain field, and if a transform function is being used for a domain's base type then it failed to enforce domain constraints on the result. Also, in many places it missed checking domain constraints on null values, because the plpy_typeio code simply wasn't called for Py_None. Rather than try to band-aid these problems, I made a significant refactoring of the plpy_typeio logic. The existing design of recursing for array and composite members is extended to also treat domains as containers requiring recursion, and the APIs for the module are cleaned up and simplified. The patch also modifies plpy_typeio to rely on the typcache more than it did before (which was pretty much not at all). This reduces the need for repetitive lookups, and lets us get rid of an ad-hoc scheme for detecting changes in composite types. I added a couple of small features to typcache to help with that. Although some of this is fixing bugs that long predate v11, I don't think we should risk a back-patch: it's a significant amount of code churn, and there've been no complaints from the field about the bugs. Tom Lane, reviewed by Anthony Bykov Discussion: https://postgr.es/m/24449.1509393613@sss.pgh.pa.us --- .../expected/hstore_plpython.out | 20 +- .../hstore_plpython/sql/hstore_plpython.sql | 16 +- src/backend/utils/cache/typcache.c | 7 + src/include/utils/typcache.h | 5 +- src/pl/plpython/expected/plpython_types.out | 128 ++ src/pl/plpython/expected/plpython_types_3.out | 128 ++ src/pl/plpython/plpy_cursorobject.c | 76 +- src/pl/plpython/plpy_cursorobject.h | 2 +- src/pl/plpython/plpy_exec.c | 173 +-- src/pl/plpython/plpy_main.c | 7 +- src/pl/plpython/plpy_planobject.h | 2 +- src/pl/plpython/plpy_procedure.c | 148 +- src/pl/plpython/plpy_procedure.h | 8 +- src/pl/plpython/plpy_spi.c | 84 +- src/pl/plpython/plpy_typeio.c | 1270 +++++++++-------- src/pl/plpython/plpy_typeio.h | 210 ++- src/pl/plpython/sql/plpython_types.sql | 91 ++ 17 files changed, 1412 insertions(+), 963 deletions(-) diff --git a/contrib/hstore_plpython/expected/hstore_plpython.out b/contrib/hstore_plpython/expected/hstore_plpython.out index df49cd5f37..1ab5feea93 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 911bbd67fe..2c54ee6aaa 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 7aadc5d6ef..f6450c402c 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 ea799a8894..c203dabbd0 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 893de301dd..eda965a9e0 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], ) + 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], ) + 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]], ) + test_type_conversion_array_domain_array +----------------------------------------- + {2,4} +(1 row) + --- --- Composite types --- @@ -820,6 +890,64 @@ SELECT test_composite_type_input(row(1, 2)); 3 (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 -- diff --git a/src/pl/plpython/expected/plpython_types_3.out b/src/pl/plpython/expected/plpython_types_3.out index 2d853bd573..69f958cbf2 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], ) + 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], ) + 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]], ) + test_type_conversion_array_domain_array +----------------------------------------- + {2,4} +(1 row) + --- --- Composite types --- @@ -820,6 +890,64 @@ SELECT test_composite_type_input(row(1, 2)); 3 (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 -- diff --git a/src/pl/plpython/plpy_cursorobject.c b/src/pl/plpython/plpy_cursorobject.c index 0108471bfe..10ca786fbc 100644 --- a/src/pl/plpython/plpy_cursorobject.c +++ b/src/pl/plpython/plpy_cursorobject.c @@ -9,6 +9,7 @@ #include #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 018b169cbf..e4d2c0ed25 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 26f61dd0f3..02d7d2ad5f 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)); - - /* - * 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. - */ 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); + /* + * 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. + */ + 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; + PLyDatumToOb *arginfo = &proc->args[i]; - 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); - } - } + 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 7df50c09c8..29db90e448 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 5adc957053..729effb163 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 26acc88b27..58d6988202 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; @@ -424,54 +413,12 @@ PLy_procedure_delete(PLyProcedure *proc) MemoryContextDelete(proc->mcxt); } -/* - * 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 d05944fc39..cd1b87fdc3 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 955769c5e3..69eb6b39f6 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 e4af8cc9ef..ce1527072e 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) -{ - 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); -} -void -PLy_output_datum_func(PLyTypeInfo *arg, 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 a Tuple"); - arg->is_rowtype = 0; - PLy_output_datum_func2(&(arg->out.d), arg->mcxt, typeTup, langid, trftypes); -} - -void -PLy_input_tuple_funcs(PLyTypeInfo *arg, TupleDesc desc) -{ - int i; + PyObject *result; 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->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)); - } - - /* 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"); - - Assert(OidIsValid(desc->tdtypeid)); + MemoryContext scratch_context = PLy_get_scratch_context(exec_ctx); + MemoryContext oldcontext; /* - * RECORDOID means we got called to create input functions for a tuple - * fetched by plpy.execute or for an anonymous record type + * 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. */ - if (desc->tdtypeid != RECORDOID) - { - HeapTuple relTup; + MemoryContextReset(scratch_context); - /* 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); + oldcontext = MemoryContextSwitchTo(scratch_context); - /* 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; + result = arg->func(arg, val); - ReleaseSysCache(relTup); - } + MemoryContextSwitchTo(oldcontext); - for (i = 0; i < desc->natts; i++) - { - HeapTuple typeTup; - Form_pg_attribute attr = TupleDescAttr(desc, i); - - if (attr->attisdropped) - continue; - - if (arg->in.r.atts[i].typoid == attr->atttypid) - 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); - } - - MemoryContextSwitchTo(oldcxt); + return result; } -void -PLy_output_tuple_funcs(PLyTypeInfo *arg, TupleDesc desc) +/* + * 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) { - 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)); - } - - Assert(OidIsValid(desc->tdtypeid)); - - /* - * RECORDOID means we got called to create output functions for an - * anonymous record type - */ - if (desc->tdtypeid != RECORDOID) - { - 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); - } - - for (i = 0; i < desc->natts; i++) - { - HeapTuple typeTup; - Form_pg_attribute attr = TupleDescAttr(desc, i); - - if (attr->attisdropped) - continue; - - if (arg->out.r.atts[i].typoid == attr->atttypid) - 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); - } - - MemoryContextSwitchTo(oldcxt); -} - -void -PLy_output_record_funcs(PLyTypeInfo *arg, TupleDesc desc) -{ - /* - * If the output record functions are already set, we just have to check - * if the record descriptor has not changed - */ - 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 - */ - Assert(arg->is_rowtype == 1); + /* at outer level, we are not considering an array element */ + return arg->func(arg, val, isnull, false); } /* * 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 * -PLyDict_FromTuple(PLyTypeInfo *info, HeapTuple tuple, TupleDesc desc) +PLy_input_from_tuple(PLyDatumToOb *arg, HeapTuple tuple, TupleDesc desc) { - PyObject *volatile dict; + PyObject *dict; PLyExecutionContext *exec_ctx = PLy_current_execution_context(); MemoryContext scratch_context = PLy_get_scratch_context(exec_ctx); - MemoryContext oldcontext = CurrentMemoryContext; + MemoryContext oldcontext; - if (info->is_rowtype != 1) - elog(ERROR, "PLyTypeInfo structure describes a datum"); + /* + * As in PLy_input_convert, do the work in the scratch context. + */ + MemoryContextReset(scratch_context); - dict = PyDict_New(); - if (dict == NULL) - PLy_elog(ERROR, "could not create new dictionary"); + oldcontext = MemoryContextSwitchTo(scratch_context); - PG_TRY(); - { - int i; + dict = PLyDict_FromTuple(arg, tuple, desc); - /* - * 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); - } - PG_CATCH(); - { - MemoryContextSwitchTo(oldcontext); - Py_DECREF(dict); - PG_RE_THROW(); - } - PG_END_TRY(); + MemoryContextSwitchTo(oldcontext); 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. + * 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. */ -Datum -PLyObject_ToCompositeDatum(PLyTypeInfo *info, TupleDesc desc, PyObject *plrv, bool inarray) +void +PLy_input_setup_tuple(PLyDatumToOb *arg, TupleDesc desc, PLyProcedure *proc) { - Datum datum; + int i; - 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); + /* We should be working on a previously-set-up struct */ + Assert(arg->func == PLyDict_FromComposite); - return datum; + /* 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; + + /* (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++) + { + Form_pg_attribute attr = TupleDescAttr(desc, i); + PLyDatumToOb *att = &arg->u.tuple.atts[i]; + + if (attr->attisdropped) + continue; + + if (att->typoid == attr->atttypid && att->typmod == attr->atttypmod) + continue; /* already set up this entry */ + + PLy_input_setup_func(att, arg->mcxt, + attr->atttypid, attr->atttypmod, + proc); + } } -static void -PLy_output_datum_func2(PLyObToDatum *arg, MemoryContext arg_mcxt, HeapTuple typeTup, Oid langid, List *trftypes) +/* + * 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_setup_tuple(PLyObToDatum *arg, TupleDesc desc, PLyProcedure *proc) { - Form_pg_type typeStruct = (Form_pg_type) GETSTRUCT(typeTup); - Oid element_type; - Oid base_type; - Oid funcid; - MemoryContext oldcxt; + int i; - oldcxt = MemoryContextSwitchTo(arg_mcxt); + /* We should be working on a previously-set-up struct */ + Assert(arg->func == PLyObject_ToComposite); - fmgr_info_cxt(typeStruct->typinput, &arg->typfunc, arg_mcxt); - arg->typoid = HeapTupleGetOid(typeTup); - arg->typmod = -1; - arg->typioparam = getTypeIOParam(typeTup); - arg->typbyval = typeStruct->typbyval; + /* 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; - element_type = get_base_element_type(arg->typoid); - base_type = getBaseType(element_type ? element_type : arg->typoid); + /* (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 = (PLyObToDatum *) + MemoryContextAllocZero(arg->mcxt, + desc->natts * sizeof(PLyObToDatum)); + } + + /* Fill the atts entries, except for dropped columns */ + for (i = 0; i < desc->natts; i++) + { + Form_pg_attribute attr = TupleDescAttr(desc, i); + PLyObToDatum *att = &arg->u.tuple.atts[i]; + + if (attr->attisdropped) + continue; + + if (att->typoid == attr->atttypid && att->typmod == attr->atttypmod) + continue; /* already set up this entry */ + + PLy_output_setup_func(att, arg->mcxt, + attr->atttypid, attr->atttypmod, + proc); + } +} + +/* + * Set up output info for a PL/Python function returning record. + * + * Note: the given tupdesc is not necessarily long-lived. + */ +void +PLy_output_setup_record(PLyObToDatum *arg, TupleDesc desc, PLyProcedure *proc) +{ + /* Makes no sense unless RECORD */ + Assert(arg->typoid == RECORDOID); + Assert(desc->tdtypeid == RECORDOID); /* - * Select a conversion function to convert Python objects to PostgreSQL - * datums. + * 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. */ + BlessTupleDesc(desc); - if ((funcid = get_transform_tosql(base_type, langid, trftypes))) + /* + * 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. + */ + 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); +} + +/* + * 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. + */ +void +PLy_output_setup_func(PLyObToDatum *arg, MemoryContext arg_mcxt, + Oid typeOid, int32 typmod, + PLyProcedure *proc) +{ + TypeCacheEntry *typentry; + char typtype; + Oid trfuncid; + Oid typinput; + + /* Since this is recursive, it could theoretically be driven to overflow */ + check_stack_depth(); + + arg->typoid = typeOid; + arg->typmod = typmod; + arg->mcxt = arg_mcxt; + + /* + * 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) { - arg->func = PLyObject_ToTransform; - fmgr_info_cxt(funcid, &arg->typtransform, arg_mcxt); - } - else if (typeStruct->typtype == TYPTYPE_COMPOSITE) - { - arg->func = PLyObject_ToComposite; + 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 - switch (base_type) + { + typentry = NULL; + typtype = TYPTYPE_COMPOSITE; + /* hard-wired knowledge about type RECORD: */ + arg->typbyval = false; + arg->typlen = -1; + arg->typalign = 'd'; + } + + /* + * 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 */ + 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(trfuncid, &arg->u.transform.typtransform, arg_mcxt); + } + 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 + { + /* 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; + TypeCacheEntry *typentry; + char typtype; + Oid trfuncid; + Oid typoutput; + bool typisvarlena; - oldcxt = MemoryContextSwitchTo(arg_mcxt); + /* Since this is recursive, it could theoretically be driven to overflow */ + check_stack_depth(); - /* 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; + arg->typoid = typeOid; + arg->typmod = typmod; + arg->mcxt = arg_mcxt; - /* Determine which kind of Python object we will convert to */ - - element_type = get_base_element_type(typeOid); - base_type = getBaseType(element_type ? element_type : typeOid); - - if ((funcid = get_transform_fromsql(base_type, langid, trftypes))) + /* + * 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) { - arg->func = PLyObject_FromTransform; - fmgr_info_cxt(funcid, &arg->typtransform, arg_mcxt); + 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 - switch (base_type) + { + typentry = NULL; + typtype = TYPTYPE_COMPOSITE; + /* hard-wired knowledge about type RECORD: */ + arg->typbyval = false; + arg->typlen = -1; + arg->typalign = 'd'; + } + + /* + * 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(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 + { + /* 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; @@ -736,6 +779,94 @@ PLyList_FromArray_recurse(PLyDatumToOb *elm, int *dims, int ndim, int dim, return list; } +/* + * 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 @@ -743,17 +874,16 @@ PLyList_FromArray_recurse(PLyDatumToOb *elm, int *dims, int ndim, int dim, * 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"); - - /* 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; - - desc = lookup_rowtype_tupdesc(arg->typoid, arg->typmod); + if (plrv == Py_None) + { + *isnull = true; + return (Datum) 0; + } + *isnull = false; /* - * 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. + * 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. */ - rv = PLyObject_ToCompositeDatum(&info, desc, plrv, inarray); + if (PyString_Check(plrv) || PyUnicode_Check(plrv)) + return PLyString_ToComposite(arg, plrv, inarray); + + /* + * 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); + } + } + + /* Simple sanity check on our caching */ + Assert(desc->natts == arg->u.tuple.natts); + + /* + * Convert, using the appropriate method depending on the type of the + * supplied Python object. + */ + 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; - - /* 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, + return InputFunctionCall(&arg->u.scalar.typfunc, str, - arg->typioparam, - typmod); + arg->u.scalar.typioparam, + arg->typmod); } +/* + * Convert to a domain type. + */ static Datum -PLyObject_ToTransform(PLyObToDatum *arg, int32 typmod, PyObject *plrv, bool inarray) +PLyObject_ToDomain(PLyObToDatum *arg, PyObject *plrv, + bool *isnull, bool inarray) { - return FunctionCall1(&arg->typtransform, PointerGetDatum(plrv)); + 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 -PLySequence_ToArray(PLyObToDatum *arg, int32 typmod, PyObject *plrv, bool inarray) +PLyObject_ToTransform(PLyObToDatum *arg, PyObject *plrv, + bool *isnull, bool inarray) +{ + 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, 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; + char *str; - /* Create a dummy PLyTypeInfo */ - cxt = AllocSetContextCreate(CurrentMemoryContext, - "PL/Python temp context", - ALLOCSET_DEFAULT_SIZES); - MemSet(&locinfo, 0, sizeof(PLyTypeInfo)); - PLy_typeinfo_init(&locinfo, cxt); + /* + * 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); - typeTup = SearchSysCache1(TYPEOID, ObjectIdGetDatum(desc->tdtypeid)); - if (!HeapTupleIsValid(typeTup)) - elog(ERROR, "cache lookup failed for type %u", desc->tdtypeid); + str = PLyObject_AsString(string); - PLy_output_datum_func2(&locinfo.out.d, locinfo.mcxt, typeTup, - exec_ctx->curr_proc->langid, - exec_ctx->curr_proc->trftypes); + /* + * 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; - ReleaseSysCache(typeTup); + /* 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',)]\"."))); + } - result = PLyObject_ToDatum(&locinfo.out.d, desc->tdtypmod, string, inarray); - - MemoryContextDelete(cxt); - - 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 95f84d8341..91870c91b0 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 { - PLyDatumToOb d; - PLyTupleToOb r; -} PLyTypeInput; + FmgrInfo typtransform; /* lookup info for from-SQL transform func */ +} PLyTransformToOb; -/* - * Conversion from Python object to a PostgreSQL Datum. - * - * 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. - */ -struct PLyObToDatum; -typedef Datum (*PLyObToDatumFunc) (struct PLyObToDatum *arg, int32 typmod, PyObject *val, bool inarray); - -typedef struct PLyObToDatum +struct PLyDatumToOb { - 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; + 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; - struct PLyObToDatum *elm; -} PLyObToDatum; + MemoryContext mcxt; /* context this info is stored in */ + union /* conversion-type-specific data */ + { + PLyScalarToOb scalar; + PLyArrayToOb array; + PLyTupleToOb tuple; + PLyTransformToOb transform; + } u; +}; + +/* + * "Output" conversion from Python object to a PostgreSQL Datum. + * + * arg is the previously-set-up conversion data, val is the value to convert. + * + * *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. + */ +typedef struct PLyObToDatum PLyObToDatum; /* forward reference */ + +typedef Datum (*PLyObToDatumFunc) (PLyObToDatum *arg, PyObject *val, + bool *isnull, + bool inarray); + +typedef struct PLyObToScalar +{ + 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; + FmgrInfo typtransform; /* lookup info for to-SQL transform function */ +} PLyObToTransform; - /* - * 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; +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; +}; - /* context for subsidiary data (doesn't belong to this struct though) */ - MemoryContext mcxt; -} PLyTypeInfo; -extern void PLy_typeinfo_init(PLyTypeInfo *arg, MemoryContext mcxt); +extern PyObject *PLy_input_convert(PLyDatumToOb *arg, Datum val); +extern Datum PLy_output_convert(PLyObToDatum *arg, PyObject *val, + bool *isnull); -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 PyObject *PLy_input_from_tuple(PLyDatumToOb *arg, HeapTuple tuple, + TupleDesc desc); -extern void PLy_input_tuple_funcs(PLyTypeInfo *arg, TupleDesc desc); -extern void PLy_output_tuple_funcs(PLyTypeInfo *arg, 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_output_record_funcs(PLyTypeInfo *arg, TupleDesc desc); +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); -/* conversion from Python objects to composite Datums */ -extern Datum PLyObject_ToCompositeDatum(PLyTypeInfo *info, TupleDesc desc, PyObject *plrv, bool isarray); +extern void PLy_output_setup_record(PLyObToDatum *arg, TupleDesc desc, + struct PLyProcedure *proc); -/* conversion from heap tuples to Python dictionaries */ -extern PyObject *PLyDict_FromTuple(PLyTypeInfo *info, HeapTuple tuple, TupleDesc desc); - -/* conversion from Python objects to C strings */ +/* 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 8c57297c24..cc0524ee80 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 --- @@ -430,6 +479,48 @@ ALTER TYPE named_pair RENAME TO named_pair_2; 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 --