diff --git a/doc/src/sgml/plpython.sgml b/doc/src/sgml/plpython.sgml index ccccf66155..a0cce540c5 100644 --- a/doc/src/sgml/plpython.sgml +++ b/doc/src/sgml/plpython.sgml @@ -1,4 +1,4 @@ - + PL/Python - Python Procedural Language @@ -46,28 +46,211 @@ PL/Python Functions - Functions in PL/Python are declared via the usual - syntax. For example: + syntax: + -CREATE FUNCTION myfunc(text) RETURNS text - AS 'return args[0]' - LANGUAGE plpythonu; +CREATE FUNCTION funcname (argument-list) + RETURNS return-type +AS $$ + # PL/Python function body +$$ LANGUAGE plpythonu; + + + + + The body of a function is simply a Python script. When the function + is called, all unnamed arguments are passed as elements to the array + args[] and named arguments as ordinary variables to the + Python script. The result is returned from the Python code in the usual way, + with return or yield (in case of + a resultset statement). + + + + For example, a function to return the greater of two integers can be + defined as: + + +CREATE FUNCTION pymax (a integer, b integer) + RETURNS integer +AS $$ + if a > b: + return a + return b +$$ LANGUAGE plpythonu; The Python code that is given as the body of the function definition - gets transformed into a Python function. - For example, the above results in + is transformed into a Python function. For example, the above results in -def __plpython_procedure_myfunc_23456(): - return args[0] +def __plpython_procedure_pymax_23456(): + if a > b: + return a + return b assuming that 23456 is the OID assigned to the function by PostgreSQL. + + The PostgreSQL function parameters are available in + the global args list. In the + pymax example, args[0] contains + whatever was passed in as the first argument and + args[1] contains the second argument's value. Alternatively, + one can use named parameters as shown in the example above. This greatly simplifies + the reading and writing of PL/Python code. + + + + If an SQL null valuenull valuePL/Python is passed to a + function, the argument value will appear as None in + Python. The above function definition will return the wrong answer for null + inputs. We could add STRICT to the function definition + to make PostgreSQL do something more reasonable: + if a null value is passed, the function will not be called at all, + but will just return a null result automatically. Alternatively, + we could check for null inputs in the function body: + + +CREATE FUNCTION pymax (a integer, b integer) + RETURNS integer +AS $$ + if (a is None) or (b is None): + return None + if a > b: + return a + return b +$$ LANGUAGE plpythonu; + + + As shown above, to return an SQL null value from a PL/Python + function, return the value None. This can be done whether the + function is strict or not. + + + + Composite-type arguments are passed to the function as Python mappings. The + element names of the mapping are the attribute names of the composite type. + If an attribute in the passed row has the null value, it has the value + None in the mapping. Here is an example: + + +CREATE TABLE employee ( + name text, + salary integer, + age integer +); + +CREATE FUNCTION overpaid (e employee) + RETURNS boolean +AS $$ + if e["salary"] > 200000: + return True + if (e["age"] < 30) and (e["salary"] > 100000): + return True + return False +$$ LANGUAGE plpythonu; + + + + + There are multiple ways to return row or composite types from a Python + scripts. In following examples we assume to have: + + +CREATE TABLE named_value ( + name text, + value integer +); + + or + +CREATE TYPE named_value AS ( + name text, + value integer +); + + + + + Sequence types (tuple or list), but not set (because + it is not indexable) + + + Returned sequence objects must have the same number of items as + composite types have fields. Item with index 0 is assigned to the first field + of the composite type, 1 to second and so on. For example: + + +CREATE FUNCTION make_pair (name text, value integer) + RETURNS named_value +AS $$ + return [ name, value ] + # or alternatively, as tuple: return ( name, value ) +$$ LANGUAGE plpythonu; + + + To return SQL null in any column, insert None at + the corresponding position. + + + + + Mapping (dictionary) + + + Value for a composite type's column is retrieved from the mapping with + the column name as key. Example: + + +CREATE FUNCTION make_pair (name text, value integer) + RETURNS named_value +AS $$ + return { "name": name, "value": value } +$$ LANGUAGE plpythonu; + + + Additional dictionary key/value pairs are ignored. Missing keys are + treated as errors, i.e. to return an SQL null value for any column, insert + None with the corresponding column name as the key. + + + + + Object (any object providing method __getattr__) + + + Example: + + +CREATE FUNCTION make_pair (name text, value integer) + RETURNS named_value +AS $$ + class named_value: + def __init__ (self, n, v): + self.name = n + self.value = v + return named_value(name, value) + + # or simply + class nv: pass + nv.name = name + nv.value = value + return nv +$$ LANGUAGE plpythonu; + + + + + + + If you do not provide a return value, Python returns the default None. PL/Python translates @@ -77,13 +260,100 @@ def __plpython_procedure_myfunc_23456(): - The PostgreSQL function parameters are available in - the global args list. In the - myfunc example, args[0] contains - whatever was passed in as the text argument. For - myfunc2(text, integer), args[0] - would contain the text argument and - args[1] the integer argument. + A PL/Python function can also return sets of + scalar or composite types. There are serveral ways to achieve this because + the returned object is internally turned into an iterator. For following + examples, let's assume to have composite type: + + +CREATE TYPE greeting AS ( + how text, + who text +); + + + Currently known iterable types are: + + + Sequence types (tuple, list, set) + + + +CREATE FUNCTION greet (how text) + RETURNS SETOF greeting +AS $$ + # return tuple containing lists as composite types + # all other combinations work also + return ( [ how, "World" ], [ how, "PostgreSQL" ], [ how, "PL/Python" ] ) +$$ LANGUAGE plpythonu; + + + + + + + Iterator (any object providing __iter__ and + next methods) + + + +CREATE FUNCTION greet (how text) + RETURNS SETOF greeting +AS $$ + class producer: + def __init__ (self, how, who): + self.how = how + self.who = who + self.ndx = -1 + + def __iter__ (self): + return self + + def next (self): + self.ndx += 1 + if self.ndx == len(self.who): + raise StopIteration + return ( self.how, self.who[self.ndx] ) + + return producer(how, [ "World", "PostgreSQL", "PL/Python" ]) +$$ LANGUAGE plpythonu; + + + + + + + Generator (yield) + + + +CREATE FUNCTION greet (how text) + RETURNS SETOF greeting +AS $$ + for who in [ "World", "PostgreSQL", "PL/Python" ]: + yield ( how, who ) +$$ LANGUAGE plpythonu; + + + + + Currently, due to Python + bug #1483133, + some debug versions of Python 2.4 + (configured and compiled with option --with-pydebug) + are known to crash the PostgreSQL server. + Unpatched versions of Fedora 4 contain this bug. + It does not happen in production version of Python or on patched + versions of Fedora 4. + + + + + + + + Whenever new iterable types are added to Python language, + PL/Python is ready to use it. diff --git a/src/pl/plpython/expected/plpython_function.out b/src/pl/plpython/expected/plpython_function.out index d367cf66aa..0e46741a27 100644 --- a/src/pl/plpython/expected/plpython_function.out +++ b/src/pl/plpython/expected/plpython_function.out @@ -55,27 +55,27 @@ except Exception, ex: return "failed, that wasn''t supposed to happen" return "succeeded, as expected"' LANGUAGE plpythonu; -CREATE FUNCTION import_test_one(text) RETURNS text +CREATE FUNCTION import_test_one(p text) RETURNS text AS 'import sha -digest = sha.new(args[0]) +digest = sha.new(p) return digest.hexdigest()' LANGUAGE plpythonu; -CREATE FUNCTION import_test_two(users) RETURNS text +CREATE FUNCTION import_test_two(u users) RETURNS text AS 'import sha -plain = args[0]["fname"] + args[0]["lname"] +plain = u["fname"] + u["lname"] digest = sha.new(plain); return "sha hash of " + plain + " is " + digest.hexdigest()' LANGUAGE plpythonu; -CREATE FUNCTION argument_test_one(users, text, text) RETURNS text +CREATE FUNCTION argument_test_one(u users, a1 text, a2 text) RETURNS text AS -'keys = args[0].keys() +'keys = u.keys() keys.sort() out = [] for key in keys: - out.append("%s: %s" % (key, args[0][key])) -words = args[1] + " " + args[2] + " => {" + ", ".join(out) + "}" + out.append("%s: %s" % (key, u[key])) +words = a1 + " " + a2 + " => {" + ", ".join(out) + "}" return words' LANGUAGE plpythonu; -- these triggers are dedicated to HPHC of RI who @@ -174,40 +174,40 @@ DROP TRIGGER show_trigger_data_trig on trigger_test; DROP FUNCTION trigger_data(); -- nested calls -- -CREATE FUNCTION nested_call_one(text) RETURNS text +CREATE FUNCTION nested_call_one(a text) RETURNS text AS -'q = "SELECT nested_call_two(''%s'')" % args[0] +'q = "SELECT nested_call_two(''%s'')" % a r = plpy.execute(q) return r[0]' LANGUAGE plpythonu ; -CREATE FUNCTION nested_call_two(text) RETURNS text +CREATE FUNCTION nested_call_two(a text) RETURNS text AS -'q = "SELECT nested_call_three(''%s'')" % args[0] +'q = "SELECT nested_call_three(''%s'')" % a r = plpy.execute(q) return r[0]' LANGUAGE plpythonu ; -CREATE FUNCTION nested_call_three(text) RETURNS text +CREATE FUNCTION nested_call_three(a text) RETURNS text AS -'return args[0]' +'return a' LANGUAGE plpythonu ; -- some spi stuff -CREATE FUNCTION spi_prepared_plan_test_one(text) RETURNS text +CREATE FUNCTION spi_prepared_plan_test_one(a text) RETURNS text AS 'if not SD.has_key("myplan"): q = "SELECT count(*) FROM users WHERE lname = $1" SD["myplan"] = plpy.prepare(q, [ "text" ]) try: - rv = plpy.execute(SD["myplan"], [args[0]]) - return "there are " + str(rv[0]["count"]) + " " + str(args[0]) + "s" + rv = plpy.execute(SD["myplan"], [a]) + return "there are " + str(rv[0]["count"]) + " " + str(a) + "s" except Exception, ex: plpy.error(str(ex)) return None ' LANGUAGE plpythonu; -CREATE FUNCTION spi_prepared_plan_test_nested(text) RETURNS text +CREATE FUNCTION spi_prepared_plan_test_nested(a text) RETURNS text AS 'if not SD.has_key("myplan"): - q = "SELECT spi_prepared_plan_test_one(''%s'') as count" % args[0] + q = "SELECT spi_prepared_plan_test_one(''%s'') as count" % a SD["myplan"] = plpy.prepare(q) try: rv = plpy.execute(SD["myplan"]) @@ -223,12 +223,12 @@ return None CREATE FUNCTION stupid() RETURNS text AS 'return "zarkon"' LANGUAGE plpythonu; /* a typo */ -CREATE FUNCTION invalid_type_uncaught(text) RETURNS text +CREATE FUNCTION invalid_type_uncaught(a text) RETURNS text AS 'if not SD.has_key("plan"): q = "SELECT fname FROM users WHERE lname = $1" SD["plan"] = plpy.prepare(q, [ "test" ]) -rv = plpy.execute(SD["plan"], [ args[0] ]) +rv = plpy.execute(SD["plan"], [ a ]) if len(rv): return rv[0]["fname"] return None @@ -237,7 +237,7 @@ return None /* for what it's worth catch the exception generated by * the typo, and return None */ -CREATE FUNCTION invalid_type_caught(text) RETURNS text +CREATE FUNCTION invalid_type_caught(a text) RETURNS text AS 'if not SD.has_key("plan"): q = "SELECT fname FROM users WHERE lname = $1" @@ -246,7 +246,7 @@ CREATE FUNCTION invalid_type_caught(text) RETURNS text except plpy.SPIError, ex: plpy.notice(str(ex)) return None -rv = plpy.execute(SD["plan"], [ args[0] ]) +rv = plpy.execute(SD["plan"], [ a ]) if len(rv): return rv[0]["fname"] return None @@ -255,7 +255,7 @@ return None /* for what it's worth catch the exception generated by * the typo, and reraise it as a plain error */ -CREATE FUNCTION invalid_type_reraised(text) RETURNS text +CREATE FUNCTION invalid_type_reraised(a text) RETURNS text AS 'if not SD.has_key("plan"): q = "SELECT fname FROM users WHERE lname = $1" @@ -263,7 +263,7 @@ CREATE FUNCTION invalid_type_reraised(text) RETURNS text SD["plan"] = plpy.prepare(q, [ "test" ]) except plpy.SPIError, ex: plpy.error(str(ex)) -rv = plpy.execute(SD["plan"], [ args[0] ]) +rv = plpy.execute(SD["plan"], [ a ]) if len(rv): return rv[0]["fname"] return None @@ -271,11 +271,11 @@ return None LANGUAGE plpythonu; /* no typo no messing about */ -CREATE FUNCTION valid_type(text) RETURNS text +CREATE FUNCTION valid_type(a text) RETURNS text AS 'if not SD.has_key("plan"): SD["plan"] = plpy.prepare("SELECT fname FROM users WHERE lname = $1", [ "text" ]) -rv = plpy.execute(SD["plan"], [ args[0] ]) +rv = plpy.execute(SD["plan"], [ a ]) if len(rv): return rv[0]["fname"] return None @@ -300,13 +300,13 @@ CREATE FUNCTION exception_index_invalid_nested() RETURNS text 'rv = plpy.execute("SELECT test5(''foo'')") return rv[0]' LANGUAGE plpythonu; -CREATE FUNCTION join_sequences(sequences) RETURNS text +CREATE FUNCTION join_sequences(s sequences) RETURNS text AS -'if not args[0]["multipart"]: - return args[0]["sequence"] -q = "SELECT sequence FROM xsequences WHERE pid = ''%s''" % args[0]["pid"] +'if not s["multipart"]: + return s["sequence"] +q = "SELECT sequence FROM xsequences WHERE pid = ''%s''" % s["pid"] rv = plpy.execute(q) -seq = args[0]["sequence"] +seq = s["sequence"] for r in rv: seq = seq + r["sequence"] return seq @@ -357,3 +357,83 @@ $$ LANGUAGE plpythonu; CREATE FUNCTION test_return_none() RETURNS int AS $$ None $$ LANGUAGE plpythonu; +-- +-- Test named parameters +-- +CREATE FUNCTION test_param_names1(a0 integer, a1 text) RETURNS boolean AS $$ +assert a0 == args[0] +assert a1 == args[1] +return True +$$ LANGUAGE plpythonu; +CREATE FUNCTION test_param_names2(u users) RETURNS text AS $$ +assert u == args[0] +return str(u) +$$ LANGUAGE plpythonu; +-- use deliberately wrong parameter names +CREATE FUNCTION test_param_names3(a0 integer) RETURNS boolean AS $$ +try: + assert a1 == args[0] + return False +except NameError, e: + assert e.args[0].find("a1") > -1 + return True +$$ LANGUAGE plpythonu; +-- +-- Test returning SETOF +-- +CREATE FUNCTION test_setof_as_list(count integer, content text) RETURNS SETOF text AS $$ +return [ content ]*count +$$ LANGUAGE plpythonu; +CREATE FUNCTION test_setof_as_tuple(count integer, content text) RETURNS SETOF text AS $$ +t = () +for i in xrange(count): + t += ( content, ) +return t +$$ LANGUAGE plpythonu; +CREATE FUNCTION test_setof_as_iterator(count integer, content text) RETURNS SETOF text AS $$ +class producer: + def __init__ (self, icount, icontent): + self.icontent = icontent + self.icount = icount + def __iter__ (self): + return self + def next (self): + if self.icount == 0: + raise StopIteration + self.icount -= 1 + return self.icontent +return producer(count, content) +$$ LANGUAGE plpythonu; +-- +-- Test returning tuples +-- +CREATE FUNCTION test_table_record_as(typ text, first text, second integer, retnull boolean) RETURNS table_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; +CREATE FUNCTION test_type_record_as(typ text, first text, second integer, retnull boolean) RETURNS type_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; diff --git a/src/pl/plpython/expected/plpython_schema.out b/src/pl/plpython/expected/plpython_schema.out index 727e4b83d9..1769ecb21f 100644 --- a/src/pl/plpython/expected/plpython_schema.out +++ b/src/pl/plpython/expected/plpython_schema.out @@ -44,3 +44,11 @@ CREATE INDEX xsequences_pid_idx ON xsequences(pid) ; CREATE TABLE unicode_test ( testvalue text NOT NULL ); +CREATE TABLE table_record ( + first text, + second int4 + ) ; +CREATE TYPE type_record AS ( + first text, + second int4 + ) ; diff --git a/src/pl/plpython/expected/plpython_test.out b/src/pl/plpython/expected/plpython_test.out index 2ccb3e94c8..170abe7ab6 100644 --- a/src/pl/plpython/expected/plpython_test.out +++ b/src/pl/plpython/expected/plpython_test.out @@ -198,3 +198,344 @@ SELECT test_return_none(), test_return_none() IS NULL AS "is null"; | t (1 row) +-- Test for functions with named parameters +SELECT test_param_names1(1,'text'); + test_param_names1 +------------------- + t +(1 row) + +SELECT test_param_names2(users) from users; + test_param_names2 +---------------------------------------------------------------------------- + {'lname': 'doe', 'username': 'j_doe', 'userid': 1, 'fname': 'jane'} + {'lname': 'doe', 'username': 'johnd', 'userid': 2, 'fname': 'john'} + {'lname': 'doe', 'username': 'w_doe', 'userid': 3, 'fname': 'willem'} + {'lname': 'smith', 'username': 'slash', 'userid': 4, 'fname': 'rick'} + {'lname': 'smith', 'username': 'w_smith', 'userid': 5, 'fname': 'willem'} + {'lname': 'darwin', 'username': 'beagle', 'userid': 6, 'fname': 'charles'} +(6 rows) + +SELECT test_param_names3(1); + test_param_names3 +------------------- + t +(1 row) + +-- Test set returning functions +SELECT test_setof_as_list(0, 'list'); + test_setof_as_list +-------------------- +(0 rows) + +SELECT test_setof_as_list(1, 'list'); + test_setof_as_list +-------------------- + list +(1 row) + +SELECT test_setof_as_list(2, 'list'); + test_setof_as_list +-------------------- + list + list +(2 rows) + +SELECT test_setof_as_list(2, null); + test_setof_as_list +-------------------- + + +(2 rows) + +SELECT test_setof_as_tuple(0, 'tuple'); + test_setof_as_tuple +--------------------- +(0 rows) + +SELECT test_setof_as_tuple(1, 'tuple'); + test_setof_as_tuple +--------------------- + tuple +(1 row) + +SELECT test_setof_as_tuple(2, 'tuple'); + test_setof_as_tuple +--------------------- + tuple + tuple +(2 rows) + +SELECT test_setof_as_tuple(2, null); + test_setof_as_tuple +--------------------- + + +(2 rows) + +SELECT test_setof_as_iterator(0, 'list'); + test_setof_as_iterator +------------------------ +(0 rows) + +SELECT test_setof_as_iterator(1, 'list'); + test_setof_as_iterator +------------------------ + list +(1 row) + +SELECT test_setof_as_iterator(2, 'list'); + test_setof_as_iterator +------------------------ + list + list +(2 rows) + +SELECT test_setof_as_iterator(2, null); + test_setof_as_iterator +------------------------ + + +(2 rows) + +-- Test tuple returning functions +SELECT * FROM test_table_record_as('dict', null, null, false); + first | second +-------+-------- + | +(1 row) + +SELECT * FROM test_table_record_as('dict', 'one', null, false); + first | second +-------+-------- + one | +(1 row) + +SELECT * FROM test_table_record_as('dict', null, 2, false); + first | second +-------+-------- + | 2 +(1 row) + +SELECT * FROM test_table_record_as('dict', 'three', 3, false); + first | second +-------+-------- + three | 3 +(1 row) + +SELECT * FROM test_table_record_as('dict', null, null, true); + first | second +-------+-------- + | +(1 row) + +SELECT * FROM test_table_record_as('tuple', null, null, false); + first | second +-------+-------- + | +(1 row) + +SELECT * FROM test_table_record_as('tuple', 'one', null, false); + first | second +-------+-------- + one | +(1 row) + +SELECT * FROM test_table_record_as('tuple', null, 2, false); + first | second +-------+-------- + | 2 +(1 row) + +SELECT * FROM test_table_record_as('tuple', 'three', 3, false); + first | second +-------+-------- + three | 3 +(1 row) + +SELECT * FROM test_table_record_as('tuple', null, null, true); + first | second +-------+-------- + | +(1 row) + +SELECT * FROM test_table_record_as('list', null, null, false); + first | second +-------+-------- + | +(1 row) + +SELECT * FROM test_table_record_as('list', 'one', null, false); + first | second +-------+-------- + one | +(1 row) + +SELECT * FROM test_table_record_as('list', null, 2, false); + first | second +-------+-------- + | 2 +(1 row) + +SELECT * FROM test_table_record_as('list', 'three', 3, false); + first | second +-------+-------- + three | 3 +(1 row) + +SELECT * FROM test_table_record_as('list', null, null, true); + first | second +-------+-------- + | +(1 row) + +SELECT * FROM test_table_record_as('obj', null, null, false); + first | second +-------+-------- + | +(1 row) + +SELECT * FROM test_table_record_as('obj', 'one', null, false); + first | second +-------+-------- + one | +(1 row) + +SELECT * FROM test_table_record_as('obj', null, 2, false); + first | second +-------+-------- + | 2 +(1 row) + +SELECT * FROM test_table_record_as('obj', 'three', 3, false); + first | second +-------+-------- + three | 3 +(1 row) + +SELECT * FROM test_table_record_as('obj', null, null, true); + first | second +-------+-------- + | +(1 row) + +SELECT * FROM test_type_record_as('dict', null, null, false); + first | second +-------+-------- + | +(1 row) + +SELECT * FROM test_type_record_as('dict', 'one', null, false); + first | second +-------+-------- + one | +(1 row) + +SELECT * FROM test_type_record_as('dict', null, 2, false); + first | second +-------+-------- + | 2 +(1 row) + +SELECT * FROM test_type_record_as('dict', 'three', 3, false); + first | second +-------+-------- + three | 3 +(1 row) + +SELECT * FROM test_type_record_as('dict', null, null, true); + first | second +-------+-------- + | +(1 row) + +SELECT * FROM test_type_record_as('tuple', null, null, false); + first | second +-------+-------- + | +(1 row) + +SELECT * FROM test_type_record_as('tuple', 'one', null, false); + first | second +-------+-------- + one | +(1 row) + +SELECT * FROM test_type_record_as('tuple', null, 2, false); + first | second +-------+-------- + | 2 +(1 row) + +SELECT * FROM test_type_record_as('tuple', 'three', 3, false); + first | second +-------+-------- + three | 3 +(1 row) + +SELECT * FROM test_type_record_as('tuple', null, null, true); + first | second +-------+-------- + | +(1 row) + +SELECT * FROM test_type_record_as('list', null, null, false); + first | second +-------+-------- + | +(1 row) + +SELECT * FROM test_type_record_as('list', 'one', null, false); + first | second +-------+-------- + one | +(1 row) + +SELECT * FROM test_type_record_as('list', null, 2, false); + first | second +-------+-------- + | 2 +(1 row) + +SELECT * FROM test_type_record_as('list', 'three', 3, false); + first | second +-------+-------- + three | 3 +(1 row) + +SELECT * FROM test_type_record_as('list', null, null, true); + first | second +-------+-------- + | +(1 row) + +SELECT * FROM test_type_record_as('obj', null, null, false); + first | second +-------+-------- + | +(1 row) + +SELECT * FROM test_type_record_as('obj', 'one', null, false); + first | second +-------+-------- + one | +(1 row) + +SELECT * FROM test_type_record_as('obj', null, 2, false); + first | second +-------+-------- + | 2 +(1 row) + +SELECT * FROM test_type_record_as('obj', 'three', 3, false); + first | second +-------+-------- + three | 3 +(1 row) + +SELECT * FROM test_type_record_as('obj', null, null, true); + first | second +-------+-------- + | +(1 row) + diff --git a/src/pl/plpython/plpython.c b/src/pl/plpython/plpython.c index f45ecd7222..40d9de37ab 100644 --- a/src/pl/plpython/plpython.c +++ b/src/pl/plpython/plpython.c @@ -1,7 +1,7 @@ /********************************************************************** * plpython.c - python as a procedural language for PostgreSQL * - * $PostgreSQL: pgsql/src/pl/plpython/plpython.c,v 1.86 2006/08/27 23:47:58 tgl Exp $ + * $PostgreSQL: pgsql/src/pl/plpython/plpython.c,v 1.87 2006/09/02 12:30:01 momjian Exp $ * ********************************************************************* */ @@ -30,6 +30,7 @@ #include "catalog/pg_type.h" #include "commands/trigger.h" #include "executor/spi.h" +#include "funcapi.h" #include "fmgr.h" #include "nodes/makefuncs.h" #include "parser/parse_type.h" @@ -121,6 +122,9 @@ typedef struct PLyProcedure bool fn_readonly; PLyTypeInfo result; /* also used to store info for trigger tuple * type */ + bool is_setof; /* true, if procedure returns result set */ + PyObject *setof; /* contents of result set. */ + char **argnames; /* Argument names */ PLyTypeInfo args[FUNC_MAX_ARGS]; int nargs; PyObject *code; /* compiled procedure code */ @@ -196,6 +200,7 @@ static Datum PLy_function_handler(FunctionCallInfo fcinfo, PLyProcedure *); static HeapTuple PLy_trigger_handler(FunctionCallInfo fcinfo, PLyProcedure *); static PyObject *PLy_function_build_args(FunctionCallInfo fcinfo, PLyProcedure *); +static void PLy_function_delete_args(PLyProcedure *); static PyObject *PLy_trigger_build_args(FunctionCallInfo fcinfo, PLyProcedure *, HeapTuple *); static HeapTuple PLy_modify_tuple(PLyProcedure *, PyObject *, @@ -231,6 +236,9 @@ static PyObject *PLyInt_FromString(const char *); static PyObject *PLyLong_FromString(const char *); static PyObject *PLyString_FromString(const char *); +static HeapTuple PLyMapping_ToTuple(PLyTypeInfo *, PyObject *); +static HeapTuple PLySequence_ToTuple(PLyTypeInfo *, PyObject *); +static HeapTuple PLyObject_ToTuple(PLyTypeInfo *, PyObject *); /* * Currently active plpython function @@ -748,11 +756,17 @@ PLy_function_handler(FunctionCallInfo fcinfo, PLyProcedure * proc) PG_TRY(); { - plargs = PLy_function_build_args(fcinfo, proc); - plrv = PLy_procedure_call(proc, "args", plargs); - - Assert(plrv != NULL); - Assert(!PLy_error_in_progress); + if (!proc->is_setof || proc->setof == NULL) + { + /* Simple type returning function or first time for SETOF function */ + plargs = PLy_function_build_args(fcinfo, proc); + plrv = PLy_procedure_call(proc, "args", plargs); + if (!proc->is_setof) + /* SETOF function parameters will be deleted when last row is returned */ + PLy_function_delete_args(proc); + Assert(plrv != NULL); + Assert(!PLy_error_in_progress); + } /* * Disconnect from SPI manager and then create the return values datum @@ -763,6 +777,67 @@ PLy_function_handler(FunctionCallInfo fcinfo, PLyProcedure * proc) if (SPI_finish() != SPI_OK_FINISH) elog(ERROR, "SPI_finish failed"); + if (proc->is_setof) + { + bool has_error = false; + ReturnSetInfo *rsi = (ReturnSetInfo *)fcinfo->resultinfo; + + if (proc->setof == NULL) + { + /* first time -- do checks and setup */ + if (!rsi || !IsA(rsi, ReturnSetInfo) || + (rsi->allowedModes & SFRM_ValuePerCall) == 0) + { + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("only value per call is allowed"))); + } + rsi->returnMode = SFRM_ValuePerCall; + + /* Make iterator out of returned object */ + proc->setof = PyObject_GetIter(plrv); + Py_DECREF(plrv); + plrv = NULL; + + if (proc->setof == NULL) + ereport(ERROR, + (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("returned object can not be iterated"), + errdetail("SETOF must be returned as iterable object"))); + } + + /* Fetch next from iterator */ + plrv = PyIter_Next(proc->setof); + if (plrv) + rsi->isDone = ExprMultipleResult; + else + { + rsi->isDone = ExprEndResult; + has_error = PyErr_Occurred() != NULL; + } + + if (rsi->isDone == ExprEndResult) + { + /* Iterator is exhausted or error happened */ + Py_DECREF(proc->setof); + proc->setof = NULL; + + Py_XDECREF(plargs); + Py_XDECREF(plrv); + Py_XDECREF(plrv_so); + + PLy_function_delete_args(proc); + + if (has_error) + ereport(ERROR, + (errcode(ERRCODE_DATA_EXCEPTION), + errmsg("error fetching next item from iterator"))); + + fcinfo->isnull = true; + return (Datum)NULL; + } + } + /* * If the function is declared to return void, the Python * return value must be None. For void-returning functions, we @@ -784,10 +859,39 @@ PLy_function_handler(FunctionCallInfo fcinfo, PLyProcedure * proc) else if (plrv == Py_None) { fcinfo->isnull = true; - rv = InputFunctionCall(&proc->result.out.d.typfunc, - NULL, - proc->result.out.d.typioparam, - -1); + 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) + { + 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) + { + fcinfo->isnull = false; + rv = HeapTupleGetDatum(tuple); + } + else + { + fcinfo->isnull = true; + rv = (Datum) NULL; + } } else { @@ -912,10 +1016,10 @@ PLy_function_build_args(FunctionCallInfo fcinfo, PLyProcedure * proc) arg = Py_None; } - /* - * FIXME -- error check this - */ - PyList_SetItem(args, i, arg); + if (PyList_SetItem(args, i, arg) == -1 || + (proc->argnames && + PyDict_SetItemString(proc->globals, proc->argnames[i], arg) == -1)) + PLy_elog(ERROR, "problem setting up arguments for \"%s\"", proc->proname); arg = NULL; } } @@ -932,6 +1036,19 @@ PLy_function_build_args(FunctionCallInfo fcinfo, PLyProcedure * proc) } +static void +PLy_function_delete_args(PLyProcedure *proc) +{ + int i; + + if (!proc->argnames) + return; + + for (i = 0; i < proc->nargs; i++) + PyDict_DelItemString(proc->globals, proc->argnames[i]); +} + + /* * PLyProcedure functions */ @@ -1002,6 +1119,9 @@ PLy_procedure_create(FunctionCallInfo fcinfo, Oid tgreloid, bool isnull; int i, rv; + Datum argnames; + Datum *elems; + int nelems; procStruct = (Form_pg_proc) GETSTRUCT(procTup); @@ -1033,6 +1153,9 @@ PLy_procedure_create(FunctionCallInfo fcinfo, Oid tgreloid, proc->nargs = 0; proc->code = proc->statics = NULL; proc->globals = proc->me = NULL; + proc->is_setof = procStruct->proretset; + proc->setof = NULL; + proc->argnames = NULL; PG_TRY(); { @@ -1069,9 +1192,11 @@ PLy_procedure_create(FunctionCallInfo fcinfo, Oid tgreloid, } if (rvTypeStruct->typtype == 'c') - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("plpython functions cannot return tuples yet"))); + { + /* Tuple: set up later, during first call to PLy_function_handler */ + proc->result.out.d.typoid = procStruct->prorettype; + proc->result.is_rowtype = 2; + } else PLy_output_datum_func(&proc->result, rvTypeTup); @@ -1094,6 +1219,20 @@ PLy_procedure_create(FunctionCallInfo fcinfo, Oid tgreloid, * arguments. */ proc->nargs = fcinfo->nargs; + if (proc->nargs) + { + argnames = SysCacheGetAttr(PROCOID, procTup, Anum_pg_proc_proargnames, &isnull); + if (!isnull) + { + deconstruct_array(DatumGetArrayTypeP(argnames), TEXTOID, -1, false, 'i', + &elems, NULL, &nelems); + if (nelems != proc->nargs) + elog(ERROR, + "proargnames must have the same number of elements " + "as the function has arguments"); + proc->argnames = (char **) PLy_malloc(sizeof(char *)*proc->nargs); + } + } for (i = 0; i < fcinfo->nargs; i++) { HeapTuple argTypeTup; @@ -1122,8 +1261,11 @@ PLy_procedure_create(FunctionCallInfo fcinfo, Oid tgreloid, proc->args[i].is_rowtype = 2; /* still need to set I/O funcs */ ReleaseSysCache(argTypeTup); - } + /* Fetch argument name */ + if (proc->argnames) + proc->argnames[i] = PLy_strdup(DatumGetCString(DirectFunctionCall1(textout, elems[i]))); + } /* * get the text of the function. @@ -1259,6 +1401,7 @@ PLy_procedure_delete(PLyProcedure * proc) if (proc->pyname) PLy_free(proc->pyname); for (i = 0; i < proc->nargs; i++) + { if (proc->args[i].is_rowtype == 1) { if (proc->args[i].in.r.atts) @@ -1266,6 +1409,11 @@ PLy_procedure_delete(PLyProcedure * proc) if (proc->args[i].out.r.atts) PLy_free(proc->args[i].out.r.atts); } + if (proc->argnames && proc->argnames[i]) + PLy_free(proc->argnames[i]); + } + if (proc->argnames) + PLy_free(proc->argnames); } /* conversion functions. remember output from python is @@ -1524,6 +1672,247 @@ PLyDict_FromTuple(PLyTypeInfo * info, HeapTuple tuple, TupleDesc desc) return dict; } + +static HeapTuple +PLyMapping_ToTuple(PLyTypeInfo *info, PyObject *mapping) +{ + TupleDesc desc; + HeapTuple tuple; + Datum *values; + char *nulls; + int i; + + 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); + + /* Build tuple */ + values = palloc(sizeof(Datum)*desc->natts); + nulls = palloc(sizeof(char)*desc->natts); + for (i = 0; i < desc->natts; ++i) + { + char *key; + PyObject *value, + *so; + + key = NameStr(desc->attrs[i]->attname); + value = so = NULL; + PG_TRY(); + { + value = PyMapping_GetItemString(mapping, key); + if (value == Py_None) + { + values[i] = (Datum) NULL; + nulls[i] = 'n'; + } + else if (value) + { + char *valuestr; + + so = PyObject_Str(value); + if (so == NULL) + PLy_elog(ERROR, "can't convert mapping type"); + valuestr = PyString_AsString(so); + + values[i] = InputFunctionCall(&info->out.r.atts[i].typfunc + , valuestr + , info->out.r.atts[i].typioparam + , -1); + Py_DECREF(so); + so = NULL; + nulls[i] = ' '; + } + else + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_COLUMN), + errmsg("no mapping found with key \"%s\"", key), + errhint("to return null in specific column, " + "add value None to map with key named after column"))); + + Py_XDECREF(value); + value = NULL; + } + PG_CATCH(); + { + Py_XDECREF(so); + Py_XDECREF(value); + PG_RE_THROW(); + } + PG_END_TRY(); + } + + tuple = heap_formtuple(desc, values, nulls); + ReleaseTupleDesc(desc); + pfree(values); + pfree(nulls); + + return tuple; +} + + +static HeapTuple +PLySequence_ToTuple(PLyTypeInfo *info, PyObject *sequence) +{ + TupleDesc desc; + HeapTuple tuple; + Datum *values; + char *nulls; + int i; + + Assert(PySequence_Check(sequence)); + + /* + * Check that sequence length is exactly same as PG tuple's. We actually + * 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); + if (PySequence_Length(sequence) != desc->natts) + ereport(ERROR, + (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("returned sequence's length must be same as tuple's length"))); + + 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(char)*desc->natts); + for (i = 0; i < desc->natts; ++i) + { + PyObject *value, + *so; + + value = so = NULL; + PG_TRY(); + { + value = PySequence_GetItem(sequence, i); + Assert(value); + if (value == Py_None) + { + values[i] = (Datum) NULL; + nulls[i] = 'n'; + } + else if (value) + { + char *valuestr; + + so = PyObject_Str(value); + if (so == NULL) + PLy_elog(ERROR, "can't convert sequence type"); + valuestr = PyString_AsString(so); + values[i] = InputFunctionCall(&info->out.r.atts[i].typfunc + , valuestr + , info->out.r.atts[i].typioparam + , -1); + Py_DECREF(so); + so = NULL; + nulls[i] = ' '; + } + + Py_XDECREF(value); + value = NULL; + } + PG_CATCH(); + { + Py_XDECREF(so); + Py_XDECREF(value); + PG_RE_THROW(); + } + PG_END_TRY(); + } + + tuple = heap_formtuple(desc, values, nulls); + ReleaseTupleDesc(desc); + pfree(values); + pfree(nulls); + + return tuple; +} + + +static HeapTuple +PLyObject_ToTuple(PLyTypeInfo *info, PyObject *object) +{ + TupleDesc desc; + HeapTuple tuple; + Datum *values; + char *nulls; + 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); + + /* Build tuple */ + values = palloc(sizeof(Datum)*desc->natts); + nulls = palloc(sizeof(char)*desc->natts); + for (i = 0; i < desc->natts; ++i) + { + char *key; + PyObject *value, + *so; + + key = NameStr(desc->attrs[i]->attname); + value = so = NULL; + PG_TRY(); + { + value = PyObject_GetAttrString(object, key); + if (value == Py_None) + { + values[i] = (Datum) NULL; + nulls[i] = 'n'; + } + else if (value) + { + char *valuestr; + + so = PyObject_Str(value); + if (so == NULL) + PLy_elog(ERROR, "can't convert object type"); + valuestr = PyString_AsString(so); + values[i] = InputFunctionCall(&info->out.r.atts[i].typfunc + , valuestr + , info->out.r.atts[i].typioparam + , -1); + Py_DECREF(so); + so = NULL; + nulls[i] = ' '; + } + else + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_COLUMN), + errmsg("no attribute named \"%s\"", key), + errhint("to return null in specific column, " + "let returned object to have attribute named " + "after column with value None"))); + + Py_XDECREF(value); + value = NULL; + } + PG_CATCH(); + { + Py_XDECREF(so); + Py_XDECREF(value); + PG_RE_THROW(); + } + PG_END_TRY(); + } + + tuple = heap_formtuple(desc, values, nulls); + ReleaseTupleDesc(desc); + pfree(values); + pfree(nulls); + + return tuple; +} + + /* initialization, some python variables function declared here */ /* interface to postgresql elog */ diff --git a/src/pl/plpython/sql/plpython_function.sql b/src/pl/plpython/sql/plpython_function.sql index e31af9f987..0450501eb9 100644 --- a/src/pl/plpython/sql/plpython_function.sql +++ b/src/pl/plpython/sql/plpython_function.sql @@ -65,29 +65,29 @@ except Exception, ex: return "succeeded, as expected"' LANGUAGE plpythonu; -CREATE FUNCTION import_test_one(text) RETURNS text +CREATE FUNCTION import_test_one(p text) RETURNS text AS 'import sha -digest = sha.new(args[0]) +digest = sha.new(p) return digest.hexdigest()' LANGUAGE plpythonu; -CREATE FUNCTION import_test_two(users) RETURNS text +CREATE FUNCTION import_test_two(u users) RETURNS text AS 'import sha -plain = args[0]["fname"] + args[0]["lname"] +plain = u["fname"] + u["lname"] digest = sha.new(plain); return "sha hash of " + plain + " is " + digest.hexdigest()' LANGUAGE plpythonu; -CREATE FUNCTION argument_test_one(users, text, text) RETURNS text +CREATE FUNCTION argument_test_one(u users, a1 text, a2 text) RETURNS text AS -'keys = args[0].keys() +'keys = u.keys() keys.sort() out = [] for key in keys: - out.append("%s: %s" % (key, args[0][key])) -words = args[1] + " " + args[2] + " => {" + ", ".join(out) + "}" + out.append("%s: %s" % (key, u[key])) +words = a1 + " " + a2 + " => {" + ", ".join(out) + "}" return words' LANGUAGE plpythonu; @@ -176,45 +176,45 @@ DROP FUNCTION trigger_data(); -- nested calls -- -CREATE FUNCTION nested_call_one(text) RETURNS text +CREATE FUNCTION nested_call_one(a text) RETURNS text AS -'q = "SELECT nested_call_two(''%s'')" % args[0] +'q = "SELECT nested_call_two(''%s'')" % a r = plpy.execute(q) return r[0]' LANGUAGE plpythonu ; -CREATE FUNCTION nested_call_two(text) RETURNS text +CREATE FUNCTION nested_call_two(a text) RETURNS text AS -'q = "SELECT nested_call_three(''%s'')" % args[0] +'q = "SELECT nested_call_three(''%s'')" % a r = plpy.execute(q) return r[0]' LANGUAGE plpythonu ; -CREATE FUNCTION nested_call_three(text) RETURNS text +CREATE FUNCTION nested_call_three(a text) RETURNS text AS -'return args[0]' +'return a' LANGUAGE plpythonu ; -- some spi stuff -CREATE FUNCTION spi_prepared_plan_test_one(text) RETURNS text +CREATE FUNCTION spi_prepared_plan_test_one(a text) RETURNS text AS 'if not SD.has_key("myplan"): q = "SELECT count(*) FROM users WHERE lname = $1" SD["myplan"] = plpy.prepare(q, [ "text" ]) try: - rv = plpy.execute(SD["myplan"], [args[0]]) - return "there are " + str(rv[0]["count"]) + " " + str(args[0]) + "s" + rv = plpy.execute(SD["myplan"], [a]) + return "there are " + str(rv[0]["count"]) + " " + str(a) + "s" except Exception, ex: plpy.error(str(ex)) return None ' LANGUAGE plpythonu; -CREATE FUNCTION spi_prepared_plan_test_nested(text) RETURNS text +CREATE FUNCTION spi_prepared_plan_test_nested(a text) RETURNS text AS 'if not SD.has_key("myplan"): - q = "SELECT spi_prepared_plan_test_one(''%s'') as count" % args[0] + q = "SELECT spi_prepared_plan_test_one(''%s'') as count" % a SD["myplan"] = plpy.prepare(q) try: rv = plpy.execute(SD["myplan"]) @@ -233,12 +233,12 @@ CREATE FUNCTION stupid() RETURNS text AS 'return "zarkon"' LANGUAGE plpythonu; /* a typo */ -CREATE FUNCTION invalid_type_uncaught(text) RETURNS text +CREATE FUNCTION invalid_type_uncaught(a text) RETURNS text AS 'if not SD.has_key("plan"): q = "SELECT fname FROM users WHERE lname = $1" SD["plan"] = plpy.prepare(q, [ "test" ]) -rv = plpy.execute(SD["plan"], [ args[0] ]) +rv = plpy.execute(SD["plan"], [ a ]) if len(rv): return rv[0]["fname"] return None @@ -248,7 +248,7 @@ return None /* for what it's worth catch the exception generated by * the typo, and return None */ -CREATE FUNCTION invalid_type_caught(text) RETURNS text +CREATE FUNCTION invalid_type_caught(a text) RETURNS text AS 'if not SD.has_key("plan"): q = "SELECT fname FROM users WHERE lname = $1" @@ -257,7 +257,7 @@ CREATE FUNCTION invalid_type_caught(text) RETURNS text except plpy.SPIError, ex: plpy.notice(str(ex)) return None -rv = plpy.execute(SD["plan"], [ args[0] ]) +rv = plpy.execute(SD["plan"], [ a ]) if len(rv): return rv[0]["fname"] return None @@ -267,7 +267,7 @@ return None /* for what it's worth catch the exception generated by * the typo, and reraise it as a plain error */ -CREATE FUNCTION invalid_type_reraised(text) RETURNS text +CREATE FUNCTION invalid_type_reraised(a text) RETURNS text AS 'if not SD.has_key("plan"): q = "SELECT fname FROM users WHERE lname = $1" @@ -275,7 +275,7 @@ CREATE FUNCTION invalid_type_reraised(text) RETURNS text SD["plan"] = plpy.prepare(q, [ "test" ]) except plpy.SPIError, ex: plpy.error(str(ex)) -rv = plpy.execute(SD["plan"], [ args[0] ]) +rv = plpy.execute(SD["plan"], [ a ]) if len(rv): return rv[0]["fname"] return None @@ -285,11 +285,11 @@ return None /* no typo no messing about */ -CREATE FUNCTION valid_type(text) RETURNS text +CREATE FUNCTION valid_type(a text) RETURNS text AS 'if not SD.has_key("plan"): SD["plan"] = plpy.prepare("SELECT fname FROM users WHERE lname = $1", [ "text" ]) -rv = plpy.execute(SD["plan"], [ args[0] ]) +rv = plpy.execute(SD["plan"], [ a ]) if len(rv): return rv[0]["fname"] return None @@ -318,13 +318,13 @@ return rv[0]' LANGUAGE plpythonu; -CREATE FUNCTION join_sequences(sequences) RETURNS text +CREATE FUNCTION join_sequences(s sequences) RETURNS text AS -'if not args[0]["multipart"]: - return args[0]["sequence"] -q = "SELECT sequence FROM xsequences WHERE pid = ''%s''" % args[0]["pid"] +'if not s["multipart"]: + return s["sequence"] +q = "SELECT sequence FROM xsequences WHERE pid = ''%s''" % s["pid"] rv = plpy.execute(q) -seq = args[0]["sequence"] +seq = s["sequence"] for r in rv: seq = seq + r["sequence"] return seq @@ -389,3 +389,95 @@ $$ LANGUAGE plpythonu; CREATE FUNCTION test_return_none() RETURNS int AS $$ None $$ LANGUAGE plpythonu; + + +-- +-- Test named parameters +-- +CREATE FUNCTION test_param_names1(a0 integer, a1 text) RETURNS boolean AS $$ +assert a0 == args[0] +assert a1 == args[1] +return True +$$ LANGUAGE plpythonu; + +CREATE FUNCTION test_param_names2(u users) RETURNS text AS $$ +assert u == args[0] +return str(u) +$$ LANGUAGE plpythonu; + +-- use deliberately wrong parameter names +CREATE FUNCTION test_param_names3(a0 integer) RETURNS boolean AS $$ +try: + assert a1 == args[0] + return False +except NameError, e: + assert e.args[0].find("a1") > -1 + return True +$$ LANGUAGE plpythonu; + + +-- +-- Test returning SETOF +-- +CREATE FUNCTION test_setof_as_list(count integer, content text) RETURNS SETOF text AS $$ +return [ content ]*count +$$ LANGUAGE plpythonu; + +CREATE FUNCTION test_setof_as_tuple(count integer, content text) RETURNS SETOF text AS $$ +t = () +for i in xrange(count): + t += ( content, ) +return t +$$ LANGUAGE plpythonu; + +CREATE FUNCTION test_setof_as_iterator(count integer, content text) RETURNS SETOF text AS $$ +class producer: + def __init__ (self, icount, icontent): + self.icontent = icontent + self.icount = icount + def __iter__ (self): + return self + def next (self): + if self.icount == 0: + raise StopIteration + self.icount -= 1 + return self.icontent +return producer(count, content) +$$ LANGUAGE plpythonu; + + +-- +-- Test returning tuples +-- +CREATE FUNCTION test_table_record_as(typ text, first text, second integer, retnull boolean) RETURNS table_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; + +CREATE FUNCTION test_type_record_as(typ text, first text, second integer, retnull boolean) RETURNS type_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; + diff --git a/src/pl/plpython/sql/plpython_schema.sql b/src/pl/plpython/sql/plpython_schema.sql index 1f5ee6eaea..c346c40381 100644 --- a/src/pl/plpython/sql/plpython_schema.sql +++ b/src/pl/plpython/sql/plpython_schema.sql @@ -42,3 +42,13 @@ CREATE INDEX xsequences_pid_idx ON xsequences(pid) ; CREATE TABLE unicode_test ( testvalue text NOT NULL ); + +CREATE TABLE table_record ( + first text, + second int4 + ) ; + +CREATE TYPE type_record AS ( + first text, + second int4 + ) ; diff --git a/src/pl/plpython/sql/plpython_test.sql b/src/pl/plpython/sql/plpython_test.sql index 2ebdb695a9..dafcae089e 100644 --- a/src/pl/plpython/sql/plpython_test.sql +++ b/src/pl/plpython/sql/plpython_test.sql @@ -73,3 +73,73 @@ SELECT newline_crlf(); SELECT test_void_func1(), test_void_func1() IS NULL AS "is null"; SELECT test_void_func2(); -- should fail SELECT test_return_none(), test_return_none() IS NULL AS "is null"; + +-- Test for functions with named parameters +SELECT test_param_names1(1,'text'); +SELECT test_param_names2(users) from users; +SELECT test_param_names3(1); + +-- Test set returning functions +SELECT test_setof_as_list(0, 'list'); +SELECT test_setof_as_list(1, 'list'); +SELECT test_setof_as_list(2, 'list'); +SELECT test_setof_as_list(2, null); + +SELECT test_setof_as_tuple(0, 'tuple'); +SELECT test_setof_as_tuple(1, 'tuple'); +SELECT test_setof_as_tuple(2, 'tuple'); +SELECT test_setof_as_tuple(2, null); + +SELECT test_setof_as_iterator(0, 'list'); +SELECT test_setof_as_iterator(1, 'list'); +SELECT test_setof_as_iterator(2, 'list'); +SELECT test_setof_as_iterator(2, null); + +-- Test tuple returning functions +SELECT * FROM test_table_record_as('dict', null, null, false); +SELECT * FROM test_table_record_as('dict', 'one', null, false); +SELECT * FROM test_table_record_as('dict', null, 2, false); +SELECT * FROM test_table_record_as('dict', 'three', 3, false); +SELECT * FROM test_table_record_as('dict', null, null, true); + +SELECT * FROM test_table_record_as('tuple', null, null, false); +SELECT * FROM test_table_record_as('tuple', 'one', null, false); +SELECT * FROM test_table_record_as('tuple', null, 2, false); +SELECT * FROM test_table_record_as('tuple', 'three', 3, false); +SELECT * FROM test_table_record_as('tuple', null, null, true); + +SELECT * FROM test_table_record_as('list', null, null, false); +SELECT * FROM test_table_record_as('list', 'one', null, false); +SELECT * FROM test_table_record_as('list', null, 2, false); +SELECT * FROM test_table_record_as('list', 'three', 3, false); +SELECT * FROM test_table_record_as('list', null, null, true); + +SELECT * FROM test_table_record_as('obj', null, null, false); +SELECT * FROM test_table_record_as('obj', 'one', null, false); +SELECT * FROM test_table_record_as('obj', null, 2, false); +SELECT * FROM test_table_record_as('obj', 'three', 3, false); +SELECT * FROM test_table_record_as('obj', null, null, true); + +SELECT * FROM test_type_record_as('dict', null, null, false); +SELECT * FROM test_type_record_as('dict', 'one', null, false); +SELECT * FROM test_type_record_as('dict', null, 2, false); +SELECT * FROM test_type_record_as('dict', 'three', 3, false); +SELECT * FROM test_type_record_as('dict', null, null, true); + +SELECT * FROM test_type_record_as('tuple', null, null, false); +SELECT * FROM test_type_record_as('tuple', 'one', null, false); +SELECT * FROM test_type_record_as('tuple', null, 2, false); +SELECT * FROM test_type_record_as('tuple', 'three', 3, false); +SELECT * FROM test_type_record_as('tuple', null, null, true); + +SELECT * FROM test_type_record_as('list', null, null, false); +SELECT * FROM test_type_record_as('list', 'one', null, false); +SELECT * FROM test_type_record_as('list', null, 2, false); +SELECT * FROM test_type_record_as('list', 'three', 3, false); +SELECT * FROM test_type_record_as('list', null, null, true); + +SELECT * FROM test_type_record_as('obj', null, null, false); +SELECT * FROM test_type_record_as('obj', 'one', null, false); +SELECT * FROM test_type_record_as('obj', null, 2, false); +SELECT * FROM test_type_record_as('obj', 'three', 3, false); +SELECT * FROM test_type_record_as('obj', null, null, true);