Here's a patch implementing table functions mentioned in
http://archives.postgresql.org/pgsql-hackers/2010-12/msg01991.php. It's
an incremental patch on top of the plpython-refactor patch sent eariler.

Git branch for this patch:
https://github.com/wulczer/postgres/tree/table-functions.

This allows functions with multiple OUT parameters returning both one or
multiple records (RECORD or SETOF RECORD). There's one inconvenience,
which is that if you return a record that has fields of composite types,
the I/O functions for these types will be looked up on each execution.
Changing that would require some juggling of the PL/Python structures,
so I just left it at that.

Note that returning just the composite type (or a set of them) does
cache the I/O funcs. You get the repeated lookups only if the function
returns an unnamed record, that has composite field among others, so
something like

CREATE FUNCTION x(OUT x table_type, OUT y integer) RETURNS RECORD

which I think is fairly uncommon.

Cheers,
Jan

diff --git a/src/pl/plpython/Makefile b/src/pl/plpython/Makefile
index 16d78ae..167393e 100644
*** a/src/pl/plpython/Makefile
--- b/src/pl/plpython/Makefile
*************** REGRESS = \
*** 79,84 ****
--- 79,85 ----
  	plpython_types \
  	plpython_error \
  	plpython_unicode \
+ 	plpython_composite \
  	plpython_drop
  # where to find psql for running the tests
  PSQLDIR = $(bindir)
diff --git a/src/pl/plpython/expected/plpython_composite.out b/src/pl/plpython/expected/plpython_composite.out
index ...70a4571 .
*** a/src/pl/plpython/expected/plpython_composite.out
--- b/src/pl/plpython/expected/plpython_composite.out
***************
*** 0 ****
--- 1,275 ----
+ CREATE FUNCTION multiout_simple(OUT i integer, OUT j integer) AS $$
+ return (1, 2)
+ $$ LANGUAGE plpythonu;
+ SELECT multiout_simple();
+  multiout_simple 
+ -----------------
+  (1,2)
+ (1 row)
+ 
+ SELECT * FROM multiout_simple();
+  i | j 
+ ---+---
+  1 | 2
+ (1 row)
+ 
+ SELECT i, j + 2 FROM multiout_simple();
+  i | ?column? 
+ ---+----------
+  1 |        4
+ (1 row)
+ 
+ SELECT (multiout_simple()).j + 3;
+  ?column? 
+ ----------
+         5
+ (1 row)
+ 
+ CREATE FUNCTION multiout_simple_setof(n integer = 1, OUT integer, OUT integer) RETURNS SETOF record AS $$
+ return [(1, 2)] * n
+ $$ LANGUAGE plpythonu;
+ SELECT multiout_simple_setof();
+  multiout_simple_setof 
+ -----------------------
+  (1,2)
+ (1 row)
+ 
+ SELECT * FROM multiout_simple_setof();
+  column1 | column2 
+ ---------+---------
+        1 |       2
+ (1 row)
+ 
+ SELECT * FROM multiout_simple_setof(3);
+  column1 | column2 
+ ---------+---------
+        1 |       2
+        1 |       2
+        1 |       2
+ (3 rows)
+ 
+ CREATE FUNCTION multiout_record_as(typ text,
+                                    first text, OUT first text,
+                                    second integer, OUT second integer,
+                                    retnull boolean) RETURNS record AS $$
+ if retnull:
+     return None
+ if typ == 'dict':
+     return { 'first': first, 'second': second, 'additionalfield': 'must not cause trouble' }
+ elif typ == 'tuple':
+     return ( first, second )
+ elif typ == 'list':
+     return [ first, second ]
+ elif typ == 'obj':
+     class type_record: pass
+     type_record.first = first
+     type_record.second = second
+     return type_record
+ $$ LANGUAGE plpythonu;
+ SELECT * from multiout_record_as('dict', 'foo', 1, 'f');
+  first | second 
+ -------+--------
+  foo   |      1
+ (1 row)
+ 
+ SELECT multiout_record_as('dict', 'foo', 1, 'f');
+  multiout_record_as 
+ --------------------
+  (foo,1)
+ (1 row)
+ 
+ SELECT *, s IS NULL as snull from multiout_record_as('tuple', 'xxx', NULL, 'f') AS f(f, s);
+   f  | s | snull 
+ -----+---+-------
+  xxx |   | t
+ (1 row)
+ 
+ SELECT *, f IS NULL as fnull, s IS NULL as snull from multiout_record_as('tuple', 'xxx', 1, 't') AS f(f, s);
+  f | s | fnull | snull 
+ ---+---+-------+-------
+    |   | t     | t
+ (1 row)
+ 
+ SELECT * from multiout_record_as('obj', NULL, 10, 'f');
+  first | second 
+ -------+--------
+        |     10
+ (1 row)
+ 
+ CREATE FUNCTION multiout_setof(n integer,
+                                OUT power_of_2 integer,
+                                OUT length integer) RETURNS SETOF record AS $$
+ for i in range(n):
+     power = 2 ** i
+     length = plpy.execute("select length('%d')" % power)[0]['length']
+     yield power, length
+ $$ LANGUAGE plpythonu;
+ SELECT * FROM multiout_setof(3);
+  power_of_2 | length 
+ ------------+--------
+           1 |      1
+           2 |      1
+           4 |      1
+ (3 rows)
+ 
+ SELECT multiout_setof(5);
+  multiout_setof 
+ ----------------
+  (1,1)
+  (2,1)
+  (4,1)
+  (8,1)
+  (16,2)
+ (5 rows)
+ 
+ CREATE FUNCTION multiout_return_table() RETURNS TABLE (x integer, y text) AS $$
+ return [{'x': 4, 'y' :'four'},
+         {'x': 7, 'y' :'seven'},
+         {'x': 0, 'y' :'zero'}]
+ $$ LANGUAGE plpythonu;
+ SELECT * FROM multiout_return_table();
+  x |   y   
+ ---+-------
+  4 | four
+  7 | seven
+  0 | zero
+ (3 rows)
+ 
+ CREATE FUNCTION multiout_array(OUT integer[], OUT text) RETURNS SETOF record AS $$
+ yield [[1], 'a']
+ yield [[1,2], 'b']
+ yield [[1,2,3], None]
+ $$ LANGUAGE plpythonu;
+ SELECT * FROM multiout_array();
+  column1 | column2 
+ ---------+---------
+  {1}     | a
+  {1,2}   | b
+  {1,2,3} | 
+ (3 rows)
+ 
+ CREATE FUNCTION singleout_composite(OUT type_record) AS $$
+ return {'first': 1, 'second': 2}
+ $$ LANGUAGE plpythonu;
+ CREATE FUNCTION multiout_composite(OUT type_record) RETURNS SETOF type_record AS $$
+ return [{'first': 1, 'second': 2},
+        {'first': 3, 'second': 4	}]
+ $$ LANGUAGE plpythonu;
+ SELECT * FROM singleout_composite();
+  first | second 
+ -------+--------
+  1     |      2
+ (1 row)
+ 
+ SELECT * FROM multiout_composite();
+  first | second 
+ -------+--------
+  1     |      2
+  3     |      4
+ (2 rows)
+ 
+ -- composite OUT parameters in functions returning RECORD not supported yet
+ CREATE FUNCTION multiout_composite(INOUT n integer, OUT type_record) AS $$
+ return (n, (n * 2, n * 3))
+ $$ LANGUAGE plpythonu;
+ CREATE FUNCTION multiout_table_type_setof(typ text, returnnull boolean, INOUT n integer, OUT table_record) RETURNS SETOF record AS $$
+ if returnnull:
+     d = None
+ elif typ == 'dict':
+     d = {'first': n * 2, 'second': n * 3, 'extra': 'not important'}
+ elif typ == 'tuple':
+     d = (n * 2, n * 3)
+ elif typ == 'obj':
+     class d: pass
+     d.first = n * 2
+     d.second = n * 3
+ for i in range(n):
+     yield (i, d)
+ $$ LANGUAGE plpythonu;
+ SELECT * FROM multiout_composite(2);
+  n | column2 
+ ---+---------
+  2 | (4,6)
+ (1 row)
+ 
+ SELECT * from multiout_table_type_setof('dict', 'f', 3);
+  n | column2 
+ ---+---------
+  0 | (6,9)
+  1 | (6,9)
+  2 | (6,9)
+ (3 rows)
+ 
+ SELECT * from multiout_table_type_setof('tuple', 'f', 2);
+  n | column2 
+ ---+---------
+  0 | (4,6)
+  1 | (4,6)
+ (2 rows)
+ 
+ SELECT * from multiout_table_type_setof('obj', 'f', 4);
+  n | column2 
+ ---+---------
+  0 | (8,12)
+  1 | (8,12)
+  2 | (8,12)
+  3 | (8,12)
+ (4 rows)
+ 
+ SELECT * from multiout_table_type_setof('dict', 't', 3);
+  n | column2 
+ ---+---------
+  0 | 
+  1 | 
+  2 | 
+ (3 rows)
+ 
+ -- check what happens if a type changes under us
+ CREATE TABLE changing (
+     i integer,
+     j integer
+ );
+ CREATE FUNCTION changing_test(OUT n integer, OUT changing) RETURNS SETOF record AS $$
+ return [(1, {'i': 1, 'j': 2}),
+         (1, (3, 4))]
+ $$ LANGUAGE plpythonu;
+ SELECT * FROM changing_test();
+  n | column2 
+ ---+---------
+  1 | (1,2)
+  1 | (3,4)
+ (2 rows)
+ 
+ ALTER TABLE changing DROP COLUMN j;
+ SELECT * FROM changing_test();
+ ERROR:  length of returned sequence did not match number of columns in row
+ CONTEXT:  while creating return value
+ PL/Python function "changing_test"
+ SELECT * FROM changing_test();
+ ERROR:  length of returned sequence did not match number of columns in row
+ CONTEXT:  while creating return value
+ PL/Python function "changing_test"
+ ALTER TABLE changing ADD COLUMN j integer;
+ SELECT * FROM changing_test();
+  n | column2 
+ ---+---------
+  1 | (1,2)
+  1 | (3,4)
+ (2 rows)
+ 
+ -- tables of composite types (not yet implemented)
+ CREATE FUNCTION composite_types_table(OUT tab table_record[], OUT typ type_record[] ) RETURNS SETOF record AS $$
+ yield {'tab': [['first', 1], ['second', 2]],
+       'typ': [{'first': 'third', 'second': 3},
+               {'first': 'fourth', 'second': 4}]}
+ yield {'tab': [['first', 1], ['second', 2]],
+       'typ': [{'first': 'third', 'second': 3},
+               {'first': 'fourth', 'second': 4}]}
+ yield {'tab': [['first', 1], ['second', 2]],
+       'typ': [{'first': 'third', 'second': 3},
+               {'first': 'fourth', 'second': 4}]}
+ $$ LANGUAGE plpythonu;
+ SELECT * FROM composite_types_table();
+ ERROR:  PL/Python functions cannot return type table_record[]
+ DETAIL:  PL/Python does not support conversion to arrays of row types.
+ CONTEXT:  PL/Python function "composite_types_table"
diff --git a/src/pl/plpython/expected/plpython_record.out b/src/pl/plpython/expected/plpython_record.out
index c8c4f9d..7c60089 100644
*** a/src/pl/plpython/expected/plpython_record.out
--- b/src/pl/plpython/expected/plpython_record.out
*************** $$ LANGUAGE plpythonu;
*** 42,51 ****
  CREATE FUNCTION test_in_out_params(first in text, second out text) AS $$
  return first + '_in_to_out';
  $$ LANGUAGE plpythonu;
- -- this doesn't work yet :-(
  CREATE FUNCTION test_in_out_params_multi(first in text,
                                           second out text, third out text) AS $$
! return first + '_record_in_to_out';
  $$ LANGUAGE plpythonu;
  CREATE FUNCTION test_inout_params(first inout text) AS $$
  return first + '_inout';
--- 42,50 ----
  CREATE FUNCTION test_in_out_params(first in text, second out text) AS $$
  return first + '_in_to_out';
  $$ LANGUAGE plpythonu;
  CREATE FUNCTION test_in_out_params_multi(first in text,
                                           second out text, third out text) AS $$
! return (first + '_record_in_to_out_1', first + '_record_in_to_out_2');
  $$ LANGUAGE plpythonu;
  CREATE FUNCTION test_inout_params(first inout text) AS $$
  return first + '_inout';
*************** SELECT * FROM test_in_out_params('test_i
*** 297,305 ****
   test_in_in_to_out
  (1 row)
  
- -- this doesn't work yet :-(
  SELECT * FROM test_in_out_params_multi('test_in');
! ERROR:  PL/Python functions cannot return type record
  SELECT * FROM test_inout_params('test_in');
       first     
  ---------------
--- 296,307 ----
   test_in_in_to_out
  (1 row)
  
  SELECT * FROM test_in_out_params_multi('test_in');
!            second           |           third            
! ----------------------------+----------------------------
!  test_in_record_in_to_out_1 | test_in_record_in_to_out_2
! (1 row)
! 
  SELECT * FROM test_inout_params('test_in');
       first     
  ---------------
diff --git a/src/pl/plpython/expected/plpython_trigger.out b/src/pl/plpython/expected/plpython_trigger.out
index e04da22..2ef66a8 100644
*** a/src/pl/plpython/expected/plpython_trigger.out
--- b/src/pl/plpython/expected/plpython_trigger.out
*************** SELECT * FROM pb;
*** 549,551 ****
--- 549,569 ----
   b | 2010-10-13 21:57:29
  (1 row)
  
+ -- triggers for tables with composite types
+ CREATE TABLE comp1 (i integer, j boolean);
+ CREATE TYPE comp2 AS (k integer, l boolean);
+ CREATE TABLE composite_trigger_test (f1 comp1, f2 comp2);
+ CREATE FUNCTION composite_trigger_f() RETURNS trigger AS $$
+     TD['new']['f1'] = (3, False)
+     TD['new']['f2'] = {'k': 7, 'l': 'yes', 'ignored': 10}
+     return 'MODIFY'
+ $$ LANGUAGE plpythonu;
+ CREATE TRIGGER composite_trigger BEFORE INSERT ON composite_trigger_test
+   FOR EACH ROW EXECUTE PROCEDURE composite_trigger_f();
+ INSERT INTO composite_trigger_test VALUES (NULL, NULL);
+ SELECT * FROM composite_trigger_test;
+   f1   |  f2   
+ -------+-------
+  (3,f) | (7,t)
+ (1 row)
+ 
diff --git a/src/pl/plpython/plpython.c b/src/pl/plpython/plpython.c
index 67eb0f3..f841779 100644
*** a/src/pl/plpython/plpython.c
--- b/src/pl/plpython/plpython.c
*************** typedef struct PLyDatumToOb
*** 129,134 ****
--- 129,135 ----
  	PLyDatumToObFunc func;
  	FmgrInfo	typfunc;		/* The type's output function */
  	Oid			typoid;			/* The OID of the type */
+ 	int32		typmod;			/* The typmod of the type */
  	Oid			typioparam;
  	bool		typbyval;
  	int16		typlen;
*************** typedef struct PLyObToDatum
*** 161,166 ****
--- 162,168 ----
  	PLyObToDatumFunc func;
  	FmgrInfo	typfunc;		/* The type's input function */
  	Oid			typoid;			/* The OID of the type */
+ 	int32		typmod;			/* The typmod of the type */
  	Oid			typioparam;
  	bool		typbyval;
  	int16		typlen;
*************** static PyObject *PLyDict_FromTuple(PLyTy
*** 349,360 ****
  
  static Datum PLyObject_ToBool(PLyObToDatum *, int32, PyObject *);
  static Datum PLyObject_ToBytea(PLyObToDatum *, int32, PyObject *);
  static Datum PLyObject_ToDatum(PLyObToDatum *, int32, PyObject *);
  static Datum PLySequence_ToArray(PLyObToDatum *, int32, PyObject *);
  
! static HeapTuple PLyMapping_ToTuple(PLyTypeInfo *, PyObject *);
! static HeapTuple PLySequence_ToTuple(PLyTypeInfo *, PyObject *);
! static HeapTuple PLyObject_ToTuple(PLyTypeInfo *, PyObject *);
  
  /*
   * Currently active plpython function
--- 351,364 ----
  
  static Datum PLyObject_ToBool(PLyObToDatum *, int32, PyObject *);
  static Datum PLyObject_ToBytea(PLyObToDatum *, int32, PyObject *);
+ static Datum PLyObject_ToComposite(PLyObToDatum *, int32, PyObject *);
  static Datum PLyObject_ToDatum(PLyObToDatum *, int32, PyObject *);
  static Datum PLySequence_ToArray(PLyObToDatum *, int32, PyObject *);
  
! static HeapTuple PLyObject_ToTuple(PLyTypeInfo *, TupleDesc, PyObject *);
! static HeapTuple PLyMapping_ToTuple(PLyTypeInfo *, TupleDesc, PyObject *);
! static HeapTuple PLySequence_ToTuple(PLyTypeInfo *, TupleDesc, PyObject *);
! static HeapTuple PLyGenericObject_ToTuple(PLyTypeInfo *, TupleDesc, PyObject *);
  
  /*
   * Currently active plpython function
*************** PLy_function_handler(FunctionCallInfo fc
*** 1112,1128 ****
  		}
  		else if (proc->result.is_rowtype >= 1)
  		{
  			HeapTuple	tuple = NULL;
  
! 			if (PySequence_Check(plrv))
! 				/* composite type as sequence (tuple, list etc) */
! 				tuple = PLySequence_ToTuple(&proc->result, plrv);
! 			else if (PyMapping_Check(plrv))
! 				/* composite type as mapping (currently only dict) */
! 				tuple = PLyMapping_ToTuple(&proc->result, plrv);
! 			else
! 				/* returned as smth, must provide method __getattr__(name) */
! 				tuple = PLyObject_ToTuple(&proc->result, plrv);
  
  			if (tuple != NULL)
  			{
--- 1116,1134 ----
  		}
  		else if (proc->result.is_rowtype >= 1)
  		{
+ 			TupleDesc	desc;
  			HeapTuple	tuple = NULL;
  
! 			/* 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);
! 
! 			tuple = PLyObject_ToTuple(&proc->result, desc, plrv);
  
  			if (tuple != NULL)
  			{
*************** PLy_function_build_args(FunctionCallInfo
*** 1254,1259 ****
--- 1260,1289 ----
  				PLy_elog(ERROR, "PyDict_SetItemString() failed, while setting up arguments");
  			arg = NULL;
  		}
+ 
+ 		/* Set up output conversion for functions returning RECORD */
+ 		if (proc->result.out.d.typoid == RECORDOID &&
+ 			proc->result.is_rowtype > 1)
+ 		{
+ 			TupleDesc	desc;
+ 
+ 			if (get_call_result_type(
+ 					fcinfo, NULL, &desc) != TYPEFUNC_COMPOSITE)
+ 			{
+ 				ereport(ERROR,
+ 						(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ 						 errmsg("function returning record called in context "
+ 								"that cannot accept type record")));
+ 			}
+ 			/* bless the record to make it known to the typcache lookup code */
+ 			BlessTupleDesc(desc);
+ 			/* save the freshly generated typmod */
+ 			proc->result.out.d.typmod = desc->tdtypmod;
+ 			/* proceed with normal I/O function caching */
+ 			PLy_output_tuple_funcs(&(proc->result), desc);
+ 			/* it should change is_rowtype to 1, so we won't go through this again */
+ 			Assert(proc->result.is_rowtype == 1);
+ 		}
  	}
  	PG_CATCH();
  	{
*************** PLy_procedure_output_conversion(PLyProce
*** 1373,1400 ****
  			 procStruct->prorettype);
  	rvTypeStruct = (Form_pg_type) GETSTRUCT(rvTypeTup);
  
! 	/* Disallow pseudotype result, except for void */
! 	if (rvTypeStruct->typtype == TYPTYPE_PSEUDO &&
! 		procStruct->prorettype != VOIDOID)
  	{
  		if (procStruct->prorettype == TRIGGEROID)
  			ereport(ERROR,
  					(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
  					 errmsg("trigger functions can only be called as triggers")));
! 		else
  			ereport(ERROR,
  					(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
  					 errmsg("PL/Python functions cannot return type %s",
  							format_type_be(procStruct->prorettype))));
  	}
  
! 	if (rvTypeStruct->typtype == TYPTYPE_COMPOSITE)
  	{
  		/*
! 		 * Tuple: set up later, during first call to
  		 * PLy_function_handler
  		 */
  		proc->result.out.d.typoid = procStruct->prorettype;
  		proc->result.is_rowtype = 2;
  	}
  	else
--- 1403,1432 ----
  			 procStruct->prorettype);
  	rvTypeStruct = (Form_pg_type) GETSTRUCT(rvTypeTup);
  
! 	/* Disallow pseudotype result, except for void or record */
! 	if (rvTypeStruct->typtype == TYPTYPE_PSEUDO)
  	{
  		if (procStruct->prorettype == TRIGGEROID)
  			ereport(ERROR,
  					(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
  					 errmsg("trigger functions can only be called as triggers")));
! 		else if (procStruct->prorettype != VOIDOID &&
! 				 procStruct->prorettype != RECORDOID)
  			ereport(ERROR,
  					(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
  					 errmsg("PL/Python functions cannot return type %s",
  							format_type_be(procStruct->prorettype))));
  	}
  
! 	if (rvTypeStruct->typtype == TYPTYPE_COMPOSITE ||
! 		procStruct->prorettype == RECORDOID)
  	{
  		/*
! 		 * Tuple or unnamed record: 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
*************** PLy_output_datum_func2(PLyObToDatum *arg
*** 1792,1797 ****
--- 1824,1830 ----
  
  	perm_fmgr_info(typeStruct->typinput, &arg->typfunc);
  	arg->typoid = HeapTupleGetOid(typeTup);
+ 	arg->typmod = -1;
  	arg->typioparam = getTypeIOParam(typeTup);
  	arg->typbyval = typeStruct->typbyval;
  
*************** PLy_output_datum_func2(PLyObToDatum *arg
*** 1814,1819 ****
--- 1847,1858 ----
  			break;
  	}
  
+ 	/* Composite types need their own input routine, though */
+ 	if (typeStruct->typtype == TYPTYPE_COMPOSITE)
+ 	{
+ 		arg->func = PLyObject_ToComposite;
+ 	}
+ 
  	if (element_type)
  	{
  		char		dummy_delim;
*************** PLy_output_datum_func2(PLyObToDatum *arg
*** 1831,1836 ****
--- 1870,1876 ----
  		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);
*************** PLy_input_datum_func2(PLyDatumToOb *arg,
*** 1856,1861 ****
--- 1896,1902 ----
  	/* Get the type's conversion information */
  	perm_fmgr_info(typeStruct->typoutput, &arg->typfunc);
  	arg->typoid = HeapTupleGetOid(typeTup);
+ 	arg->typmod = -1;
  	arg->typioparam = getTypeIOParam(typeTup);
  	arg->typbyval = typeStruct->typbyval;
  	arg->typlen = typeStruct->typlen;
*************** PLy_input_datum_func2(PLyDatumToOb *arg,
*** 1902,1907 ****
--- 1943,1949 ----
  		arg->elm->func = arg->func;
  		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);
*************** PLyDict_FromTuple(PLyTypeInfo *info, Hea
*** 2102,2107 ****
--- 2144,2172 ----
  }
  
  /*
+  *  Convert a Python object to a PostgreSQL tuple, using all supported
+  *  conversion methods: tuple as a sequence, as a mapping or as an object that
+  *  has __getattr__ support.
+  */
+ static HeapTuple
+ PLyObject_ToTuple(PLyTypeInfo *info, TupleDesc desc, PyObject *plrv)
+ {
+ 	HeapTuple	tuple;
+ 
+ 	if (PySequence_Check(plrv))
+ 		/* composite type as sequence (tuple, list etc) */
+ 		tuple = PLySequence_ToTuple(info, desc, plrv);
+ 	else if (PyMapping_Check(plrv))
+ 		/* composite type as mapping (currently only dict) */
+ 		tuple = PLyMapping_ToTuple(info, desc, plrv);
+ 	else
+ 		/* returned as smth, must provide method __getattr__(name) */
+ 		tuple = PLyGenericObject_ToTuple(info, desc, plrv);
+ 
+ 	return tuple;
+ }
+ 
+ /*
   * Convert a Python object to a PostgreSQL bool datum.	This can't go
   * through the generic conversion function, because Python attaches a
   * Boolean value to everything, more things than the PostgreSQL bool
*************** PLyObject_ToBytea(PLyObToDatum *arg, int
*** 2164,2169 ****
--- 2229,2278 ----
  	return rv;
  }
  
+ 
+ /*
+  * Convert a Python object to a composite type. First look up the type's
+  * description, then route the Python object through the conversion function
+  * for obtaining PostgreSQL tuples.
+  */
+ static Datum
+ PLyObject_ToComposite(PLyObToDatum *arg, int32 typmod, PyObject *plrv)
+ {
+ 	HeapTuple	tuple = NULL;
+ 	Datum		rv;
+ 	PLyTypeInfo	info;
+ 	TupleDesc	desc;
+ 
+ 	if (typmod != -1)
+ 		elog(ERROR, "received unnamed record type as input");
+ 
+ 	/* Create a dummy PLyTypeInfo */
+ 	MemSet(&info, 0, sizeof(PLyTypeInfo));
+ 	PLy_typeinfo_init(&info);
+ 	/* Mark it as needing output routines lookup */
+ 	info.is_rowtype = 2;
+ 
+ 	desc = lookup_rowtype_tupdesc(arg->typoid, arg->typmod);
+ 
+ 	/*
+ 	 * This will set up the dummy PLyTypeInfo's output conversion routines,
+ 	 * since we left is_rowtype as 2. A future optimisation could be caching
+ 	 * that info instead of looking it up every time a tuple is returned from
+ 	 * the function.
+ 	 */
+ 	tuple = PLyObject_ToTuple(&info, desc, plrv);
+ 
+ 	PLy_typeinfo_dealloc(&info);
+ 
+ 	if (tuple != NULL)
+ 		rv = HeapTupleGetDatum(tuple);
+ 	else
+ 		rv = (Datum) NULL;
+ 
+ 	return rv;
+ }
+ 
+ 
  /*
   * Generic conversion function: Convert PyObject to cstring and
   * cstring into PostgreSQL type.
*************** PLySequence_ToArray(PLyObToDatum *arg, i
*** 2267,2275 ****
  }
  
  static HeapTuple
! PLyMapping_ToTuple(PLyTypeInfo *info, PyObject *mapping)
  {
- 	TupleDesc	desc;
  	HeapTuple	tuple;
  	Datum	   *values;
  	bool	   *nulls;
--- 2376,2383 ----
  }
  
  static HeapTuple
! PLyMapping_ToTuple(PLyTypeInfo *info, TupleDesc desc, PyObject *mapping)
  {
  	HeapTuple	tuple;
  	Datum	   *values;
  	bool	   *nulls;
*************** PLyMapping_ToTuple(PLyTypeInfo *info, Py
*** 2277,2283 ****
  
  	Assert(PyMapping_Check(mapping));
  
- 	desc = lookup_rowtype_tupdesc(info->out.d.typoid, -1);
  	if (info->is_rowtype == 2)
  		PLy_output_tuple_funcs(info, desc);
  	Assert(info->is_rowtype == 1);
--- 2385,2390 ----
*************** PLyMapping_ToTuple(PLyTypeInfo *info, Py
*** 2338,2346 ****
  
  
  static HeapTuple
! PLySequence_ToTuple(PLyTypeInfo *info, PyObject *sequence)
  {
- 	TupleDesc	desc;
  	HeapTuple	tuple;
  	Datum	   *values;
  	bool	   *nulls;
--- 2445,2452 ----
  
  
  static HeapTuple
! PLySequence_ToTuple(PLyTypeInfo *info, TupleDesc desc, PyObject *sequence)
  {
  	HeapTuple	tuple;
  	Datum	   *values;
  	bool	   *nulls;
*************** PLySequence_ToTuple(PLyTypeInfo *info, P
*** 2353,2359 ****
  	 * can ignore exceeding items or assume missing ones as null but to avoid
  	 * plpython developer's errors we are strict here
  	 */
- 	desc = lookup_rowtype_tupdesc(info->out.d.typoid, -1);
  	idx = 0;
  	for (i = 0; i < desc->natts; ++i)
  	{
--- 2459,2464 ----
*************** PLySequence_ToTuple(PLyTypeInfo *info, P
*** 2421,2435 ****
  
  
  static HeapTuple
! PLyObject_ToTuple(PLyTypeInfo *info, PyObject *object)
  {
- 	TupleDesc	desc;
  	HeapTuple	tuple;
  	Datum	   *values;
  	bool	   *nulls;
  	volatile int i;
  
- 	desc = lookup_rowtype_tupdesc(info->out.d.typoid, -1);
  	if (info->is_rowtype == 2)
  		PLy_output_tuple_funcs(info, desc);
  	Assert(info->is_rowtype == 1);
--- 2526,2538 ----
  
  
  static HeapTuple
! PLyGenericObject_ToTuple(PLyTypeInfo *info, TupleDesc desc, PyObject *object)
  {
  	HeapTuple	tuple;
  	Datum	   *values;
  	bool	   *nulls;
  	volatile int i;
  
  	if (info->is_rowtype == 2)
  		PLy_output_tuple_funcs(info, desc);
  	Assert(info->is_rowtype == 1);
diff --git a/src/pl/plpython/sql/plpython_composite.sql b/src/pl/plpython/sql/plpython_composite.sql
index ...1330e26 .
*** a/src/pl/plpython/sql/plpython_composite.sql
--- b/src/pl/plpython/sql/plpython_composite.sql
***************
*** 0 ****
--- 1,142 ----
+ CREATE FUNCTION multiout_simple(OUT i integer, OUT j integer) AS $$
+ return (1, 2)
+ $$ LANGUAGE plpythonu;
+ 
+ SELECT multiout_simple();
+ SELECT * FROM multiout_simple();
+ SELECT i, j + 2 FROM multiout_simple();
+ SELECT (multiout_simple()).j + 3;
+ 
+ CREATE FUNCTION multiout_simple_setof(n integer = 1, OUT integer, OUT integer) RETURNS SETOF record AS $$
+ return [(1, 2)] * n
+ $$ LANGUAGE plpythonu;
+ 
+ SELECT multiout_simple_setof();
+ SELECT * FROM multiout_simple_setof();
+ SELECT * FROM multiout_simple_setof(3);
+ 
+ CREATE FUNCTION multiout_record_as(typ text,
+                                    first text, OUT first text,
+                                    second integer, OUT second integer,
+                                    retnull boolean) RETURNS record AS $$
+ if retnull:
+     return None
+ if typ == 'dict':
+     return { 'first': first, 'second': second, 'additionalfield': 'must not cause trouble' }
+ elif typ == 'tuple':
+     return ( first, second )
+ elif typ == 'list':
+     return [ first, second ]
+ elif typ == 'obj':
+     class type_record: pass
+     type_record.first = first
+     type_record.second = second
+     return type_record
+ $$ LANGUAGE plpythonu;
+ 
+ SELECT * from multiout_record_as('dict', 'foo', 1, 'f');
+ SELECT multiout_record_as('dict', 'foo', 1, 'f');
+ SELECT *, s IS NULL as snull from multiout_record_as('tuple', 'xxx', NULL, 'f') AS f(f, s);
+ SELECT *, f IS NULL as fnull, s IS NULL as snull from multiout_record_as('tuple', 'xxx', 1, 't') AS f(f, s);
+ SELECT * from multiout_record_as('obj', NULL, 10, 'f');
+ 
+ CREATE FUNCTION multiout_setof(n integer,
+                                OUT power_of_2 integer,
+                                OUT length integer) RETURNS SETOF record AS $$
+ for i in range(n):
+     power = 2 ** i
+     length = plpy.execute("select length('%d')" % power)[0]['length']
+     yield power, length
+ $$ LANGUAGE plpythonu;
+ 
+ SELECT * FROM multiout_setof(3);
+ SELECT multiout_setof(5);
+ 
+ CREATE FUNCTION multiout_return_table() RETURNS TABLE (x integer, y text) AS $$
+ return [{'x': 4, 'y' :'four'},
+         {'x': 7, 'y' :'seven'},
+         {'x': 0, 'y' :'zero'}]
+ $$ LANGUAGE plpythonu;
+ 
+ SELECT * FROM multiout_return_table();
+ 
+ CREATE FUNCTION multiout_array(OUT integer[], OUT text) RETURNS SETOF record AS $$
+ yield [[1], 'a']
+ yield [[1,2], 'b']
+ yield [[1,2,3], None]
+ $$ LANGUAGE plpythonu;
+ 
+ SELECT * FROM multiout_array();
+ 
+ CREATE FUNCTION singleout_composite(OUT type_record) AS $$
+ return {'first': 1, 'second': 2}
+ $$ LANGUAGE plpythonu;
+ 
+ CREATE FUNCTION multiout_composite(OUT type_record) RETURNS SETOF type_record AS $$
+ return [{'first': 1, 'second': 2},
+        {'first': 3, 'second': 4	}]
+ $$ LANGUAGE plpythonu;
+ 
+ SELECT * FROM singleout_composite();
+ SELECT * FROM multiout_composite();
+ 
+ -- composite OUT parameters in functions returning RECORD not supported yet
+ CREATE FUNCTION multiout_composite(INOUT n integer, OUT type_record) AS $$
+ return (n, (n * 2, n * 3))
+ $$ LANGUAGE plpythonu;
+ 
+ CREATE FUNCTION multiout_table_type_setof(typ text, returnnull boolean, INOUT n integer, OUT table_record) RETURNS SETOF record AS $$
+ if returnnull:
+     d = None
+ elif typ == 'dict':
+     d = {'first': n * 2, 'second': n * 3, 'extra': 'not important'}
+ elif typ == 'tuple':
+     d = (n * 2, n * 3)
+ elif typ == 'obj':
+     class d: pass
+     d.first = n * 2
+     d.second = n * 3
+ for i in range(n):
+     yield (i, d)
+ $$ LANGUAGE plpythonu;
+ 
+ SELECT * FROM multiout_composite(2);
+ SELECT * from multiout_table_type_setof('dict', 'f', 3);
+ SELECT * from multiout_table_type_setof('tuple', 'f', 2);
+ SELECT * from multiout_table_type_setof('obj', 'f', 4);
+ SELECT * from multiout_table_type_setof('dict', 't', 3);
+ 
+ -- check what happens if a type changes under us
+ 
+ CREATE TABLE changing (
+     i integer,
+     j integer
+ );
+ 
+ CREATE FUNCTION changing_test(OUT n integer, OUT changing) RETURNS SETOF record AS $$
+ return [(1, {'i': 1, 'j': 2}),
+         (1, (3, 4))]
+ $$ LANGUAGE plpythonu;
+ 
+ SELECT * FROM changing_test();
+ ALTER TABLE changing DROP COLUMN j;
+ SELECT * FROM changing_test();
+ SELECT * FROM changing_test();
+ ALTER TABLE changing ADD COLUMN j integer;
+ SELECT * FROM changing_test();
+ 
+ -- tables of composite types (not yet implemented)
+ 
+ CREATE FUNCTION composite_types_table(OUT tab table_record[], OUT typ type_record[] ) RETURNS SETOF record AS $$
+ yield {'tab': [['first', 1], ['second', 2]],
+       'typ': [{'first': 'third', 'second': 3},
+               {'first': 'fourth', 'second': 4}]}
+ yield {'tab': [['first', 1], ['second', 2]],
+       'typ': [{'first': 'third', 'second': 3},
+               {'first': 'fourth', 'second': 4}]}
+ yield {'tab': [['first', 1], ['second', 2]],
+       'typ': [{'first': 'third', 'second': 3},
+               {'first': 'fourth', 'second': 4}]}
+ $$ LANGUAGE plpythonu;
+ 
+ SELECT * FROM composite_types_table();
diff --git a/src/pl/plpython/sql/plpython_record.sql b/src/pl/plpython/sql/plpython_record.sql
index 5a41565..d727e60 100644
*** a/src/pl/plpython/sql/plpython_record.sql
--- b/src/pl/plpython/sql/plpython_record.sql
*************** CREATE FUNCTION test_in_out_params(first
*** 49,58 ****
  return first + '_in_to_out';
  $$ LANGUAGE plpythonu;
  
- -- this doesn't work yet :-(
  CREATE FUNCTION test_in_out_params_multi(first in text,
                                           second out text, third out text) AS $$
! return first + '_record_in_to_out';
  $$ LANGUAGE plpythonu;
  
  CREATE FUNCTION test_inout_params(first inout text) AS $$
--- 49,57 ----
  return first + '_in_to_out';
  $$ LANGUAGE plpythonu;
  
  CREATE FUNCTION test_in_out_params_multi(first in text,
                                           second out text, third out text) AS $$
! return (first + '_record_in_to_out_1', first + '_record_in_to_out_2');
  $$ LANGUAGE plpythonu;
  
  CREATE FUNCTION test_inout_params(first inout text) AS $$
*************** SELECT * FROM test_type_record_as('obj',
*** 110,116 ****
  SELECT * FROM test_type_record_as('obj', null, null, true);
  
  SELECT * FROM test_in_out_params('test_in');
- -- this doesn't work yet :-(
  SELECT * FROM test_in_out_params_multi('test_in');
  SELECT * FROM test_inout_params('test_in');
  
--- 109,114 ----
diff --git a/src/pl/plpython/sql/plpython_trigger.sql b/src/pl/plpython/sql/plpython_trigger.sql
index 4994d8f..2afdf51 100644
*** a/src/pl/plpython/sql/plpython_trigger.sql
--- b/src/pl/plpython/sql/plpython_trigger.sql
*************** INSERT INTO pb VALUES ('a', '2010-10-09
*** 326,328 ****
--- 326,348 ----
  SELECT * FROM pb;
  UPDATE pb SET a = 'b';
  SELECT * FROM pb;
+ 
+ 
+ -- triggers for tables with composite types
+ 
+ CREATE TABLE comp1 (i integer, j boolean);
+ CREATE TYPE comp2 AS (k integer, l boolean);
+ 
+ CREATE TABLE composite_trigger_test (f1 comp1, f2 comp2);
+ 
+ CREATE FUNCTION composite_trigger_f() RETURNS trigger AS $$
+     TD['new']['f1'] = (3, False)
+     TD['new']['f2'] = {'k': 7, 'l': 'yes', 'ignored': 10}
+     return 'MODIFY'
+ $$ LANGUAGE plpythonu;
+ 
+ CREATE TRIGGER composite_trigger BEFORE INSERT ON composite_trigger_test
+   FOR EACH ROW EXECUTE PROCEDURE composite_trigger_f();
+ 
+ INSERT INTO composite_trigger_test VALUES (NULL, NULL);
+ SELECT * FROM composite_trigger_test;
-- 
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

Reply via email to