summaryrefslogtreecommitdiffstats
path: root/src/pl/plpython/sql
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-16 19:46:48 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-16 19:46:48 +0000
commit311bcfc6b3acdd6fd152798c7f287ddf74fa2a98 (patch)
tree0ec307299b1dada3701e42f4ca6eda57d708261e /src/pl/plpython/sql
parentInitial commit. (diff)
downloadpostgresql-15-311bcfc6b3acdd6fd152798c7f287ddf74fa2a98.tar.xz
postgresql-15-311bcfc6b3acdd6fd152798c7f287ddf74fa2a98.zip
Adding upstream version 15.4.upstream/15.4upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'src/pl/plpython/sql')
-rw-r--r--src/pl/plpython/sql/plpython_call.sql80
-rw-r--r--src/pl/plpython/sql/plpython_composite.sql224
-rw-r--r--src/pl/plpython/sql/plpython_do.sql3
-rw-r--r--src/pl/plpython/sql/plpython_drop.sql6
-rw-r--r--src/pl/plpython/sql/plpython_ereport.sql135
-rw-r--r--src/pl/plpython/sql/plpython_error.sql346
-rw-r--r--src/pl/plpython/sql/plpython_global.sql38
-rw-r--r--src/pl/plpython/sql/plpython_import.sql68
-rw-r--r--src/pl/plpython/sql/plpython_newline.sql20
-rw-r--r--src/pl/plpython/sql/plpython_params.sql42
-rw-r--r--src/pl/plpython/sql/plpython_populate.sql27
-rw-r--r--src/pl/plpython/sql/plpython_quote.sql33
-rw-r--r--src/pl/plpython/sql/plpython_record.sql163
-rw-r--r--src/pl/plpython/sql/plpython_schema.sql39
-rw-r--r--src/pl/plpython/sql/plpython_setof.sql97
-rw-r--r--src/pl/plpython/sql/plpython_spi.sql322
-rw-r--r--src/pl/plpython/sql/plpython_subtransaction.sql262
-rw-r--r--src/pl/plpython/sql/plpython_test.sql52
-rw-r--r--src/pl/plpython/sql/plpython_transaction.sql182
-rw-r--r--src/pl/plpython/sql/plpython_trigger.sql469
-rw-r--r--src/pl/plpython/sql/plpython_types.sql622
-rw-r--r--src/pl/plpython/sql/plpython_unicode.sql45
-rw-r--r--src/pl/plpython/sql/plpython_void.sql22
23 files changed, 3297 insertions, 0 deletions
diff --git a/src/pl/plpython/sql/plpython_call.sql b/src/pl/plpython/sql/plpython_call.sql
new file mode 100644
index 0000000..daa4bc3
--- /dev/null
+++ b/src/pl/plpython/sql/plpython_call.sql
@@ -0,0 +1,80 @@
+--
+-- Tests for procedures / CALL syntax
+--
+
+CREATE PROCEDURE test_proc1()
+LANGUAGE plpython3u
+AS $$
+pass
+$$;
+
+CALL test_proc1();
+
+
+-- error: can't return non-None
+CREATE PROCEDURE test_proc2()
+LANGUAGE plpython3u
+AS $$
+return 5
+$$;
+
+CALL test_proc2();
+
+
+CREATE TABLE test1 (a int);
+
+CREATE PROCEDURE test_proc3(x int)
+LANGUAGE plpython3u
+AS $$
+plpy.execute("INSERT INTO test1 VALUES (%s)" % x)
+$$;
+
+CALL test_proc3(55);
+
+SELECT * FROM test1;
+
+
+-- output arguments
+
+CREATE PROCEDURE test_proc5(INOUT a text)
+LANGUAGE plpython3u
+AS $$
+return [a + '+' + a]
+$$;
+
+CALL test_proc5('abc');
+
+
+CREATE PROCEDURE test_proc6(a int, INOUT b int, INOUT c int)
+LANGUAGE plpython3u
+AS $$
+return (b * a, c * a)
+$$;
+
+CALL test_proc6(2, 3, 4);
+
+
+-- OUT parameters
+
+CREATE PROCEDURE test_proc9(IN a int, OUT b int)
+LANGUAGE plpython3u
+AS $$
+plpy.notice("a: %s" % (a))
+return (a * 2,)
+$$;
+
+DO $$
+DECLARE _a int; _b int;
+BEGIN
+ _a := 10; _b := 30;
+ CALL test_proc9(_a, _b);
+ RAISE NOTICE '_a: %, _b: %', _a, _b;
+END
+$$;
+
+
+DROP PROCEDURE test_proc1;
+DROP PROCEDURE test_proc2;
+DROP PROCEDURE test_proc3;
+
+DROP TABLE test1;
diff --git a/src/pl/plpython/sql/plpython_composite.sql b/src/pl/plpython/sql/plpython_composite.sql
new file mode 100644
index 0000000..2175770
--- /dev/null
+++ b/src/pl/plpython/sql/plpython_composite.sql
@@ -0,0 +1,224 @@
+CREATE FUNCTION multiout_simple(OUT i integer, OUT j integer) AS $$
+return (1, 2)
+$$ LANGUAGE plpython3u;
+
+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 plpython3u;
+
+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
+elif typ == 'str':
+ return "('%s',%r)" % (first, second)
+$$ LANGUAGE plpython3u;
+
+SELECT * FROM multiout_record_as('dict', 'foo', 1, 'f');
+SELECT multiout_record_as('dict', 'foo', 1, 'f');
+
+SELECT * FROM multiout_record_as('dict', null, null, false);
+SELECT * FROM multiout_record_as('dict', 'one', null, false);
+SELECT * FROM multiout_record_as('dict', null, 2, false);
+SELECT * FROM multiout_record_as('dict', 'three', 3, false);
+SELECT * FROM multiout_record_as('dict', null, null, true);
+
+SELECT * FROM multiout_record_as('tuple', null, null, false);
+SELECT * FROM multiout_record_as('tuple', 'one', null, false);
+SELECT * FROM multiout_record_as('tuple', null, 2, false);
+SELECT * FROM multiout_record_as('tuple', 'three', 3, false);
+SELECT * FROM multiout_record_as('tuple', null, null, true);
+
+SELECT * FROM multiout_record_as('list', null, null, false);
+SELECT * FROM multiout_record_as('list', 'one', null, false);
+SELECT * FROM multiout_record_as('list', null, 2, false);
+SELECT * FROM multiout_record_as('list', 'three', 3, false);
+SELECT * FROM multiout_record_as('list', null, null, true);
+
+SELECT * FROM multiout_record_as('obj', null, null, false);
+SELECT * FROM multiout_record_as('obj', 'one', null, false);
+SELECT * FROM multiout_record_as('obj', null, 2, false);
+SELECT * FROM multiout_record_as('obj', 'three', 3, false);
+SELECT * FROM multiout_record_as('obj', null, null, true);
+
+SELECT * FROM multiout_record_as('str', 'one', 1, false);
+SELECT * FROM multiout_record_as('str', 'one', 2, false);
+
+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 plpython3u;
+
+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 plpython3u;
+
+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 plpython3u;
+
+SELECT * FROM multiout_array();
+
+CREATE FUNCTION singleout_composite(OUT type_record) AS $$
+return {'first': 1, 'second': 2}
+$$ LANGUAGE plpython3u;
+
+CREATE FUNCTION multiout_composite(OUT type_record) RETURNS SETOF type_record AS $$
+return [{'first': 1, 'second': 2},
+ {'first': 3, 'second': 4 }]
+$$ LANGUAGE plpython3u;
+
+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 plpython3u;
+
+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 == 'list':
+ d = [ n * 2, n * 3 ]
+elif typ == 'obj':
+ class d: pass
+ d.first = n * 2
+ d.second = n * 3
+elif typ == 'str':
+ d = "(%r,%r)" % (n * 2, n * 3)
+for i in range(n):
+ yield (i, d)
+$$ LANGUAGE plpython3u;
+
+SELECT * FROM multiout_composite(2);
+SELECT * FROM multiout_table_type_setof('dict', 'f', 3);
+SELECT * FROM multiout_table_type_setof('dict', 'f', 7);
+SELECT * FROM multiout_table_type_setof('tuple', 'f', 2);
+SELECT * FROM multiout_table_type_setof('tuple', 'f', 3);
+SELECT * FROM multiout_table_type_setof('list', 'f', 2);
+SELECT * FROM multiout_table_type_setof('list', 'f', 3);
+SELECT * FROM multiout_table_type_setof('obj', 'f', 4);
+SELECT * FROM multiout_table_type_setof('obj', 'f', 5);
+SELECT * FROM multiout_table_type_setof('str', 'f', 6);
+SELECT * FROM multiout_table_type_setof('str', 'f', 7);
+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 plpython3u;
+
+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
+
+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 plpython3u;
+
+SELECT * FROM composite_types_table();
+
+-- check what happens if the output record descriptor changes
+CREATE FUNCTION return_record(t text) RETURNS record AS $$
+return {'t': t, 'val': 10}
+$$ LANGUAGE plpython3u;
+
+SELECT * FROM return_record('abc') AS r(t text, val integer);
+SELECT * FROM return_record('abc') AS r(t text, val bigint);
+SELECT * FROM return_record('abc') AS r(t text, val integer);
+SELECT * FROM return_record('abc') AS r(t varchar(30), val integer);
+SELECT * FROM return_record('abc') AS r(t varchar(100), val integer);
+SELECT * FROM return_record('999') AS r(val text, t integer);
+
+CREATE FUNCTION return_record_2(t text) RETURNS record AS $$
+return {'v1':1,'v2':2,t:3}
+$$ LANGUAGE plpython3u;
+
+SELECT * FROM return_record_2('v3') AS (v3 int, v2 int, v1 int);
+SELECT * FROM return_record_2('v3') AS (v2 int, v3 int, v1 int);
+SELECT * FROM return_record_2('v4') AS (v1 int, v4 int, v2 int);
+SELECT * FROM return_record_2('v4') AS (v1 int, v4 int, v2 int);
+-- error
+SELECT * FROM return_record_2('v4') AS (v1 int, v3 int, v2 int);
+-- works
+SELECT * FROM return_record_2('v3') AS (v1 int, v3 int, v2 int);
+SELECT * FROM return_record_2('v3') AS (v1 int, v2 int, v3 int);
+
+-- multi-dimensional array of composite types.
+CREATE FUNCTION composite_type_as_list() RETURNS type_record[] AS $$
+ return [[('first', 1), ('second', 1)], [('first', 2), ('second', 2)], [('first', 3), ('second', 3)]];
+$$ LANGUAGE plpython3u;
+SELECT * FROM composite_type_as_list();
+
+-- Starting with PostgreSQL 10, a composite type in an array cannot be
+-- represented as a Python list, because it's ambiguous with multi-dimensional
+-- arrays. So this throws an error now. The error should contain a useful hint
+-- on the issue.
+CREATE FUNCTION composite_type_as_list_broken() RETURNS type_record[] AS $$
+ return [['first', 1]];
+$$ LANGUAGE plpython3u;
+SELECT * FROM composite_type_as_list_broken();
diff --git a/src/pl/plpython/sql/plpython_do.sql b/src/pl/plpython/sql/plpython_do.sql
new file mode 100644
index 0000000..d494132
--- /dev/null
+++ b/src/pl/plpython/sql/plpython_do.sql
@@ -0,0 +1,3 @@
+DO $$ plpy.notice("This is plpython3u.") $$ LANGUAGE plpython3u;
+
+DO $$ raise Exception("error test") $$ LANGUAGE plpython3u;
diff --git a/src/pl/plpython/sql/plpython_drop.sql b/src/pl/plpython/sql/plpython_drop.sql
new file mode 100644
index 0000000..e4f373b
--- /dev/null
+++ b/src/pl/plpython/sql/plpython_drop.sql
@@ -0,0 +1,6 @@
+--
+-- For paranoia's sake, don't leave an untrusted language sitting around
+--
+SET client_min_messages = WARNING;
+
+DROP EXTENSION plpython3u CASCADE;
diff --git a/src/pl/plpython/sql/plpython_ereport.sql b/src/pl/plpython/sql/plpython_ereport.sql
new file mode 100644
index 0000000..d4f6223
--- /dev/null
+++ b/src/pl/plpython/sql/plpython_ereport.sql
@@ -0,0 +1,135 @@
+CREATE FUNCTION elog_test() RETURNS void
+AS $$
+plpy.debug('debug', detail='some detail')
+plpy.log('log', detail='some detail')
+plpy.info('info', detail='some detail')
+plpy.info()
+plpy.info('the question', detail=42);
+plpy.info('This is message text.',
+ detail='This is detail text',
+ hint='This is hint text.',
+ sqlstate='XX000',
+ schema_name='any info about schema',
+ table_name='any info about table',
+ column_name='any info about column',
+ datatype_name='any info about datatype',
+ constraint_name='any info about constraint')
+plpy.notice('notice', detail='some detail')
+plpy.warning('warning', detail='some detail')
+plpy.error('stop on error', detail='some detail', hint='some hint')
+$$ LANGUAGE plpython3u;
+
+SELECT elog_test();
+
+DO $$ plpy.info('other types', detail=(10, 20)) $$ LANGUAGE plpython3u;
+
+DO $$
+import time;
+from datetime import date
+plpy.info('other types', detail=date(2016, 2, 26))
+$$ LANGUAGE plpython3u;
+
+DO $$
+basket = ['apple', 'orange', 'apple', 'pear', 'orange', 'banana']
+plpy.info('other types', detail=basket)
+$$ LANGUAGE plpython3u;
+
+-- should fail
+DO $$ plpy.info('wrong sqlstate', sqlstate='54444A') $$ LANGUAGE plpython3u;
+DO $$ plpy.info('unsupported argument', blabla='fooboo') $$ LANGUAGE plpython3u;
+DO $$ plpy.info('first message', message='second message') $$ LANGUAGE plpython3u;
+DO $$ plpy.info('first message', 'second message', message='third message') $$ LANGUAGE plpython3u;
+
+-- raise exception in python, handle exception in plgsql
+CREATE OR REPLACE FUNCTION raise_exception(_message text, _detail text DEFAULT NULL, _hint text DEFAULT NULL,
+ _sqlstate text DEFAULT NULL,
+ _schema_name text DEFAULT NULL,
+ _table_name text DEFAULT NULL,
+ _column_name text DEFAULT NULL,
+ _datatype_name text DEFAULT NULL,
+ _constraint_name text DEFAULT NULL)
+RETURNS void AS $$
+kwargs = {
+ "message": _message, "detail": _detail, "hint": _hint,
+ "sqlstate": _sqlstate, "schema_name": _schema_name, "table_name": _table_name,
+ "column_name": _column_name, "datatype_name": _datatype_name,
+ "constraint_name": _constraint_name
+}
+# ignore None values
+plpy.error(**dict((k, v) for k, v in iter(kwargs.items()) if v))
+$$ LANGUAGE plpython3u;
+
+SELECT raise_exception('hello', 'world');
+SELECT raise_exception('message text', 'detail text', _sqlstate => 'YY333');
+SELECT raise_exception(_message => 'message text',
+ _detail => 'detail text',
+ _hint => 'hint text',
+ _sqlstate => 'XX555',
+ _schema_name => 'schema text',
+ _table_name => 'table text',
+ _column_name => 'column text',
+ _datatype_name => 'datatype text',
+ _constraint_name => 'constraint text');
+
+SELECT raise_exception(_message => 'message text',
+ _hint => 'hint text',
+ _schema_name => 'schema text',
+ _column_name => 'column text',
+ _constraint_name => 'constraint text');
+
+DO $$
+DECLARE
+ __message text;
+ __detail text;
+ __hint text;
+ __sqlstate text;
+ __schema_name text;
+ __table_name text;
+ __column_name text;
+ __datatype_name text;
+ __constraint_name text;
+BEGIN
+ BEGIN
+ PERFORM raise_exception(_message => 'message text',
+ _detail => 'detail text',
+ _hint => 'hint text',
+ _sqlstate => 'XX555',
+ _schema_name => 'schema text',
+ _table_name => 'table text',
+ _column_name => 'column text',
+ _datatype_name => 'datatype text',
+ _constraint_name => 'constraint text');
+ EXCEPTION WHEN SQLSTATE 'XX555' THEN
+ GET STACKED DIAGNOSTICS __message = MESSAGE_TEXT,
+ __detail = PG_EXCEPTION_DETAIL,
+ __hint = PG_EXCEPTION_HINT,
+ __sqlstate = RETURNED_SQLSTATE,
+ __schema_name = SCHEMA_NAME,
+ __table_name = TABLE_NAME,
+ __column_name = COLUMN_NAME,
+ __datatype_name = PG_DATATYPE_NAME,
+ __constraint_name = CONSTRAINT_NAME;
+ RAISE NOTICE 'handled exception'
+ USING DETAIL = format('message:(%s), detail:(%s), hint: (%s), sqlstate: (%s), '
+ 'schema_name:(%s), table_name:(%s), column_name:(%s), datatype_name:(%s), constraint_name:(%s)',
+ __message, __detail, __hint, __sqlstate, __schema_name,
+ __table_name, __column_name, __datatype_name, __constraint_name);
+ END;
+END;
+$$;
+
+DO $$
+try:
+ plpy.execute("select raise_exception(_message => 'my message', _sqlstate => 'XX987', _hint => 'some hint', _table_name => 'users_tab', _datatype_name => 'user_type')")
+except Exception as e:
+ plpy.info(e.spidata)
+ raise e
+$$ LANGUAGE plpython3u;
+
+DO $$
+try:
+ plpy.error(message = 'my message', sqlstate = 'XX987', hint = 'some hint', table_name = 'users_tab', datatype_name = 'user_type')
+except Exception as e:
+ plpy.info('sqlstate: %s, hint: %s, table_name: %s, datatype_name: %s' % (e.sqlstate, e.hint, e.table_name, e.datatype_name))
+ raise e
+$$ LANGUAGE plpython3u;
diff --git a/src/pl/plpython/sql/plpython_error.sql b/src/pl/plpython/sql/plpython_error.sql
new file mode 100644
index 0000000..11f14ec
--- /dev/null
+++ b/src/pl/plpython/sql/plpython_error.sql
@@ -0,0 +1,346 @@
+-- test error handling, i forgot to restore Warn_restart in
+-- the trigger handler once. the errors and subsequent core dump were
+-- interesting.
+
+/* Flat out Python syntax error
+ */
+CREATE FUNCTION python_syntax_error() RETURNS text
+ AS
+'.syntaxerror'
+ LANGUAGE plpython3u;
+
+/* With check_function_bodies = false the function should get defined
+ * and the error reported when called
+ */
+SET check_function_bodies = false;
+
+CREATE FUNCTION python_syntax_error() RETURNS text
+ AS
+'.syntaxerror'
+ LANGUAGE plpython3u;
+
+SELECT python_syntax_error();
+/* Run the function twice to check if the hashtable entry gets cleaned up */
+SELECT python_syntax_error();
+
+RESET check_function_bodies;
+
+/* Flat out syntax error
+ */
+CREATE FUNCTION sql_syntax_error() RETURNS text
+ AS
+'plpy.execute("syntax error")'
+ LANGUAGE plpython3u;
+
+SELECT sql_syntax_error();
+
+
+/* check the handling of uncaught python exceptions
+ */
+CREATE FUNCTION exception_index_invalid(text) RETURNS text
+ AS
+'return args[1]'
+ LANGUAGE plpython3u;
+
+SELECT exception_index_invalid('test');
+
+
+/* check handling of nested exceptions
+ */
+CREATE FUNCTION exception_index_invalid_nested() RETURNS text
+ AS
+'rv = plpy.execute("SELECT test5(''foo'')")
+return rv[0]'
+ LANGUAGE plpython3u;
+
+SELECT exception_index_invalid_nested();
+
+
+/* a typo
+ */
+CREATE FUNCTION invalid_type_uncaught(a text) RETURNS text
+ AS
+'if "plan" not in SD:
+ q = "SELECT fname FROM users WHERE lname = $1"
+ SD["plan"] = plpy.prepare(q, [ "test" ])
+rv = plpy.execute(SD["plan"], [ a ])
+if len(rv):
+ return rv[0]["fname"]
+return None
+'
+ LANGUAGE plpython3u;
+
+SELECT invalid_type_uncaught('rick');
+
+
+/* for what it's worth catch the exception generated by
+ * the typo, and return None
+ */
+CREATE FUNCTION invalid_type_caught(a text) RETURNS text
+ AS
+'if "plan" not in SD:
+ q = "SELECT fname FROM users WHERE lname = $1"
+ try:
+ SD["plan"] = plpy.prepare(q, [ "test" ])
+ except plpy.SPIError as ex:
+ plpy.notice(str(ex))
+ return None
+rv = plpy.execute(SD["plan"], [ a ])
+if len(rv):
+ return rv[0]["fname"]
+return None
+'
+ LANGUAGE plpython3u;
+
+SELECT invalid_type_caught('rick');
+
+
+/* for what it's worth catch the exception generated by
+ * the typo, and reraise it as a plain error
+ */
+CREATE FUNCTION invalid_type_reraised(a text) RETURNS text
+ AS
+'if "plan" not in SD:
+ q = "SELECT fname FROM users WHERE lname = $1"
+ try:
+ SD["plan"] = plpy.prepare(q, [ "test" ])
+ except plpy.SPIError as ex:
+ plpy.error(str(ex))
+rv = plpy.execute(SD["plan"], [ a ])
+if len(rv):
+ return rv[0]["fname"]
+return None
+'
+ LANGUAGE plpython3u;
+
+SELECT invalid_type_reraised('rick');
+
+
+/* no typo no messing about
+ */
+CREATE FUNCTION valid_type(a text) RETURNS text
+ AS
+'if "plan" not in SD:
+ SD["plan"] = plpy.prepare("SELECT fname FROM users WHERE lname = $1", [ "text" ])
+rv = plpy.execute(SD["plan"], [ a ])
+if len(rv):
+ return rv[0]["fname"]
+return None
+'
+ LANGUAGE plpython3u;
+
+SELECT valid_type('rick');
+
+/* error in nested functions to get a traceback
+*/
+CREATE FUNCTION nested_error() RETURNS text
+ AS
+'def fun1():
+ plpy.error("boom")
+
+def fun2():
+ fun1()
+
+def fun3():
+ fun2()
+
+fun3()
+return "not reached"
+'
+ LANGUAGE plpython3u;
+
+SELECT nested_error();
+
+/* raising plpy.Error is just like calling plpy.error
+*/
+CREATE FUNCTION nested_error_raise() RETURNS text
+ AS
+'def fun1():
+ raise plpy.Error("boom")
+
+def fun2():
+ fun1()
+
+def fun3():
+ fun2()
+
+fun3()
+return "not reached"
+'
+ LANGUAGE plpython3u;
+
+SELECT nested_error_raise();
+
+/* using plpy.warning should not produce a traceback
+*/
+CREATE FUNCTION nested_warning() RETURNS text
+ AS
+'def fun1():
+ plpy.warning("boom")
+
+def fun2():
+ fun1()
+
+def fun3():
+ fun2()
+
+fun3()
+return "you''ve been warned"
+'
+ LANGUAGE plpython3u;
+
+SELECT nested_warning();
+
+/* AttributeError at toplevel used to give segfaults with the traceback
+*/
+CREATE FUNCTION toplevel_attribute_error() RETURNS void AS
+$$
+plpy.nonexistent
+$$ LANGUAGE plpython3u;
+
+SELECT toplevel_attribute_error();
+
+/* Calling PL/Python functions from SQL and vice versa should not lose context.
+ */
+CREATE OR REPLACE FUNCTION python_traceback() RETURNS void AS $$
+def first():
+ second()
+
+def second():
+ third()
+
+def third():
+ plpy.execute("select sql_error()")
+
+first()
+$$ LANGUAGE plpython3u;
+
+CREATE OR REPLACE FUNCTION sql_error() RETURNS void AS $$
+begin
+ select 1/0;
+end
+$$ LANGUAGE plpgsql;
+
+CREATE OR REPLACE FUNCTION python_from_sql_error() RETURNS void AS $$
+begin
+ select python_traceback();
+end
+$$ LANGUAGE plpgsql;
+
+CREATE OR REPLACE FUNCTION sql_from_python_error() RETURNS void AS $$
+plpy.execute("select sql_error()")
+$$ LANGUAGE plpython3u;
+
+SELECT python_traceback();
+SELECT sql_error();
+SELECT python_from_sql_error();
+SELECT sql_from_python_error();
+
+/* check catching specific types of exceptions
+ */
+CREATE TABLE specific (
+ i integer PRIMARY KEY
+);
+
+CREATE FUNCTION specific_exception(i integer) RETURNS void AS
+$$
+from plpy import spiexceptions
+try:
+ plpy.execute("insert into specific values (%s)" % (i or "NULL"));
+except spiexceptions.NotNullViolation as e:
+ plpy.notice("Violated the NOT NULL constraint, sqlstate %s" % e.sqlstate)
+except spiexceptions.UniqueViolation as e:
+ plpy.notice("Violated the UNIQUE constraint, sqlstate %s" % e.sqlstate)
+$$ LANGUAGE plpython3u;
+
+SELECT specific_exception(2);
+SELECT specific_exception(NULL);
+SELECT specific_exception(2);
+
+/* SPI errors in PL/Python functions should preserve the SQLSTATE value
+ */
+CREATE FUNCTION python_unique_violation() RETURNS void AS $$
+plpy.execute("insert into specific values (1)")
+plpy.execute("insert into specific values (1)")
+$$ LANGUAGE plpython3u;
+
+CREATE FUNCTION catch_python_unique_violation() RETURNS text AS $$
+begin
+ begin
+ perform python_unique_violation();
+ exception when unique_violation then
+ return 'ok';
+ end;
+ return 'not reached';
+end;
+$$ language plpgsql;
+
+SELECT catch_python_unique_violation();
+
+/* manually starting subtransactions - a bad idea
+ */
+CREATE FUNCTION manual_subxact() RETURNS void AS $$
+plpy.execute("savepoint save")
+plpy.execute("create table foo(x integer)")
+plpy.execute("rollback to save")
+$$ LANGUAGE plpython3u;
+
+SELECT manual_subxact();
+
+/* same for prepared plans
+ */
+CREATE FUNCTION manual_subxact_prepared() RETURNS void AS $$
+save = plpy.prepare("savepoint save")
+rollback = plpy.prepare("rollback to save")
+plpy.execute(save)
+plpy.execute("create table foo(x integer)")
+plpy.execute(rollback)
+$$ LANGUAGE plpython3u;
+
+SELECT manual_subxact_prepared();
+
+/* raising plpy.spiexception.* from python code should preserve sqlstate
+ */
+CREATE FUNCTION plpy_raise_spiexception() RETURNS void AS $$
+raise plpy.spiexceptions.DivisionByZero()
+$$ LANGUAGE plpython3u;
+
+DO $$
+BEGIN
+ SELECT plpy_raise_spiexception();
+EXCEPTION WHEN division_by_zero THEN
+ -- NOOP
+END
+$$ LANGUAGE plpgsql;
+
+/* setting a custom sqlstate should be handled
+ */
+CREATE FUNCTION plpy_raise_spiexception_override() RETURNS void AS $$
+exc = plpy.spiexceptions.DivisionByZero()
+exc.sqlstate = 'SILLY'
+raise exc
+$$ LANGUAGE plpython3u;
+
+DO $$
+BEGIN
+ SELECT plpy_raise_spiexception_override();
+EXCEPTION WHEN SQLSTATE 'SILLY' THEN
+ -- NOOP
+END
+$$ LANGUAGE plpgsql;
+
+/* test the context stack trace for nested execution levels
+ */
+CREATE FUNCTION notice_innerfunc() RETURNS int AS $$
+plpy.execute("DO LANGUAGE plpython3u $x$ plpy.notice('inside DO') $x$")
+return 1
+$$ LANGUAGE plpython3u;
+
+CREATE FUNCTION notice_outerfunc() RETURNS int AS $$
+plpy.execute("SELECT notice_innerfunc()")
+return 1
+$$ LANGUAGE plpython3u;
+
+\set SHOW_CONTEXT always
+
+SELECT notice_outerfunc();
diff --git a/src/pl/plpython/sql/plpython_global.sql b/src/pl/plpython/sql/plpython_global.sql
new file mode 100644
index 0000000..96d2049
--- /dev/null
+++ b/src/pl/plpython/sql/plpython_global.sql
@@ -0,0 +1,38 @@
+--
+-- check static and global data (SD and GD)
+--
+
+CREATE FUNCTION global_test_one() returns text
+ AS
+'if "global_test" not in SD:
+ SD["global_test"] = "set by global_test_one"
+if "global_test" not in GD:
+ GD["global_test"] = "set by global_test_one"
+return "SD: " + SD["global_test"] + ", GD: " + GD["global_test"]'
+ LANGUAGE plpython3u;
+
+CREATE FUNCTION global_test_two() returns text
+ AS
+'if "global_test" not in SD:
+ SD["global_test"] = "set by global_test_two"
+if "global_test" not in GD:
+ GD["global_test"] = "set by global_test_two"
+return "SD: " + SD["global_test"] + ", GD: " + GD["global_test"]'
+ LANGUAGE plpython3u;
+
+
+CREATE FUNCTION static_test() returns int4
+ AS
+'if "call" in SD:
+ SD["call"] = SD["call"] + 1
+else:
+ SD["call"] = 1
+return SD["call"]
+'
+ LANGUAGE plpython3u;
+
+
+SELECT static_test();
+SELECT static_test();
+SELECT global_test_one();
+SELECT global_test_two();
diff --git a/src/pl/plpython/sql/plpython_import.sql b/src/pl/plpython/sql/plpython_import.sql
new file mode 100644
index 0000000..3031eef
--- /dev/null
+++ b/src/pl/plpython/sql/plpython_import.sql
@@ -0,0 +1,68 @@
+-- import python modules
+
+CREATE FUNCTION import_fail() returns text
+ AS
+'try:
+ import foosocket
+except ImportError:
+ return "failed as expected"
+return "succeeded, that wasn''t supposed to happen"'
+ LANGUAGE plpython3u;
+
+
+CREATE FUNCTION import_succeed() returns text
+ AS
+'try:
+ import array
+ import bisect
+ import calendar
+ import cmath
+ import errno
+ import math
+ import operator
+ import random
+ import re
+ import string
+ import time
+except Exception as ex:
+ plpy.notice("import failed -- %s" % str(ex))
+ return "failed, that wasn''t supposed to happen"
+return "succeeded, as expected"'
+ LANGUAGE plpython3u;
+
+CREATE FUNCTION import_test_one(p text) RETURNS text
+ AS
+'try:
+ import hashlib
+ digest = hashlib.sha1(p.encode("ascii"))
+except ImportError:
+ import sha
+ digest = sha.new(p)
+return digest.hexdigest()'
+ LANGUAGE plpython3u;
+
+CREATE FUNCTION import_test_two(u users) RETURNS text
+ AS
+'plain = u["fname"] + u["lname"]
+try:
+ import hashlib
+ digest = hashlib.sha1(plain.encode("ascii"))
+except ImportError:
+ import sha
+ digest = sha.new(plain);
+return "sha hash of " + plain + " is " + digest.hexdigest()'
+ LANGUAGE plpython3u;
+
+
+-- import python modules
+--
+SELECT import_fail();
+SELECT import_succeed();
+
+-- test import and simple argument handling
+--
+SELECT import_test_one('sha hash of this string');
+
+-- test import and tuple argument handling
+--
+select import_test_two(users) from users where fname = 'willem';
diff --git a/src/pl/plpython/sql/plpython_newline.sql b/src/pl/plpython/sql/plpython_newline.sql
new file mode 100644
index 0000000..cb22ba9
--- /dev/null
+++ b/src/pl/plpython/sql/plpython_newline.sql
@@ -0,0 +1,20 @@
+--
+-- Universal Newline Support
+--
+
+CREATE OR REPLACE FUNCTION newline_lf() RETURNS integer AS
+E'x = 100\ny = 23\nreturn x + y\n'
+LANGUAGE plpython3u;
+
+CREATE OR REPLACE FUNCTION newline_cr() RETURNS integer AS
+E'x = 100\ry = 23\rreturn x + y\r'
+LANGUAGE plpython3u;
+
+CREATE OR REPLACE FUNCTION newline_crlf() RETURNS integer AS
+E'x = 100\r\ny = 23\r\nreturn x + y\r\n'
+LANGUAGE plpython3u;
+
+
+SELECT newline_lf();
+SELECT newline_cr();
+SELECT newline_crlf();
diff --git a/src/pl/plpython/sql/plpython_params.sql b/src/pl/plpython/sql/plpython_params.sql
new file mode 100644
index 0000000..8bab488
--- /dev/null
+++ b/src/pl/plpython/sql/plpython_params.sql
@@ -0,0 +1,42 @@
+--
+-- Test named and nameless parameters
+--
+
+CREATE FUNCTION test_param_names0(integer, integer) RETURNS int AS $$
+return args[0] + args[1]
+$$ LANGUAGE plpython3u;
+
+CREATE FUNCTION test_param_names1(a0 integer, a1 text) RETURNS boolean AS $$
+assert a0 == args[0]
+assert a1 == args[1]
+return True
+$$ LANGUAGE plpython3u;
+
+CREATE FUNCTION test_param_names2(u users) RETURNS text AS $$
+assert u == args[0]
+if isinstance(u, dict):
+ # stringify dict the hard way because otherwise the order is implementation-dependent
+ u_keys = list(u.keys())
+ u_keys.sort()
+ s = '{' + ', '.join([repr(k) + ': ' + repr(u[k]) for k in u_keys]) + '}'
+else:
+ s = str(u)
+return s
+$$ LANGUAGE plpython3u;
+
+-- use deliberately wrong parameter names
+CREATE FUNCTION test_param_names3(a0 integer) RETURNS boolean AS $$
+try:
+ assert a1 == args[0]
+ return False
+except NameError as e:
+ assert e.args[0].find("a1") > -1
+ return True
+$$ LANGUAGE plpython3u;
+
+
+SELECT test_param_names0(2,7);
+SELECT test_param_names1(1,'text');
+SELECT test_param_names2(users) from users;
+SELECT test_param_names2(NULL);
+SELECT test_param_names3(1);
diff --git a/src/pl/plpython/sql/plpython_populate.sql b/src/pl/plpython/sql/plpython_populate.sql
new file mode 100644
index 0000000..cc1e19b
--- /dev/null
+++ b/src/pl/plpython/sql/plpython_populate.sql
@@ -0,0 +1,27 @@
+INSERT INTO users (fname, lname, username) VALUES ('jane', 'doe', 'j_doe');
+INSERT INTO users (fname, lname, username) VALUES ('john', 'doe', 'johnd');
+INSERT INTO users (fname, lname, username) VALUES ('willem', 'doe', 'w_doe');
+INSERT INTO users (fname, lname, username) VALUES ('rick', 'smith', 'slash');
+
+
+-- multi table tests
+--
+
+INSERT INTO taxonomy (name) VALUES ('HIV I') ;
+INSERT INTO taxonomy (name) VALUES ('HIV II') ;
+INSERT INTO taxonomy (name) VALUES ('HCV') ;
+
+INSERT INTO entry (accession, txid) VALUES ('A00001', '1') ;
+INSERT INTO entry (accession, txid) VALUES ('A00002', '1') ;
+INSERT INTO entry (accession, txid) VALUES ('A00003', '1') ;
+INSERT INTO entry (accession, txid) VALUES ('A00004', '2') ;
+INSERT INTO entry (accession, txid) VALUES ('A00005', '2') ;
+INSERT INTO entry (accession, txid) VALUES ('A00006', '3') ;
+
+INSERT INTO sequences (sequence, eid, product, multipart) VALUES ('ABCDEF', 1, 'env', 'true') ;
+INSERT INTO xsequences (sequence, pid) VALUES ('GHIJKL', 1) ;
+INSERT INTO sequences (sequence, eid, product) VALUES ('ABCDEF', 2, 'env') ;
+INSERT INTO sequences (sequence, eid, product) VALUES ('ABCDEF', 3, 'env') ;
+INSERT INTO sequences (sequence, eid, product) VALUES ('ABCDEF', 4, 'gag') ;
+INSERT INTO sequences (sequence, eid, product) VALUES ('ABCDEF', 5, 'env') ;
+INSERT INTO sequences (sequence, eid, product) VALUES ('ABCDEF', 6, 'ns1') ;
diff --git a/src/pl/plpython/sql/plpython_quote.sql b/src/pl/plpython/sql/plpython_quote.sql
new file mode 100644
index 0000000..a1133e7
--- /dev/null
+++ b/src/pl/plpython/sql/plpython_quote.sql
@@ -0,0 +1,33 @@
+-- test quoting functions
+
+CREATE FUNCTION quote(t text, how text) RETURNS text AS $$
+ if how == "literal":
+ return plpy.quote_literal(t)
+ elif how == "nullable":
+ return plpy.quote_nullable(t)
+ elif how == "ident":
+ return plpy.quote_ident(t)
+ else:
+ raise plpy.Error("unrecognized quote type %s" % how)
+$$ LANGUAGE plpython3u;
+
+SELECT quote(t, 'literal') FROM (VALUES
+ ('abc'),
+ ('a''bc'),
+ ('''abc'''),
+ (''),
+ (''''),
+ ('xyzv')) AS v(t);
+
+SELECT quote(t, 'nullable') FROM (VALUES
+ ('abc'),
+ ('a''bc'),
+ ('''abc'''),
+ (''),
+ (''''),
+ (NULL)) AS v(t);
+
+SELECT quote(t, 'ident') FROM (VALUES
+ ('abc'),
+ ('a b c'),
+ ('a " ''abc''')) AS v(t);
diff --git a/src/pl/plpython/sql/plpython_record.sql b/src/pl/plpython/sql/plpython_record.sql
new file mode 100644
index 0000000..52bad8b
--- /dev/null
+++ b/src/pl/plpython/sql/plpython_record.sql
@@ -0,0 +1,163 @@
+--
+-- Test returning tuples
+--
+
+CREATE TABLE table_record (
+ first text,
+ second int4
+ ) ;
+
+CREATE TYPE type_record AS (
+ first text,
+ second int4
+ ) ;
+
+
+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 plpython3u;
+
+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
+elif typ == 'str':
+ return "('%s',%r)" % (first, second)
+$$ LANGUAGE plpython3u;
+
+CREATE FUNCTION test_in_out_params(first in text, second out text) AS $$
+return first + '_in_to_out';
+$$ LANGUAGE plpython3u;
+
+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 plpython3u;
+
+CREATE FUNCTION test_inout_params(first inout text) AS $$
+return first + '_inout';
+$$ LANGUAGE plpython3u;
+
+
+-- 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);
+
+SELECT * FROM test_type_record_as('str', 'one', 1, false);
+
+SELECT * FROM test_in_out_params('test_in');
+SELECT * FROM test_in_out_params_multi('test_in');
+SELECT * FROM test_inout_params('test_in');
+
+-- try changing the return types and call functions again
+
+ALTER TABLE table_record DROP COLUMN first;
+ALTER TABLE table_record DROP COLUMN second;
+ALTER TABLE table_record ADD COLUMN first text;
+ALTER TABLE table_record ADD COLUMN second int4;
+
+SELECT * FROM test_table_record_as('obj', 'one', 1, false);
+
+ALTER TYPE type_record DROP ATTRIBUTE first;
+ALTER TYPE type_record DROP ATTRIBUTE second;
+ALTER TYPE type_record ADD ATTRIBUTE first text;
+ALTER TYPE type_record ADD ATTRIBUTE second int4;
+
+SELECT * FROM test_type_record_as('obj', 'one', 1, false);
+
+-- errors cases
+
+CREATE FUNCTION test_type_record_error1() RETURNS type_record AS $$
+ return { 'first': 'first' }
+$$ LANGUAGE plpython3u;
+
+SELECT * FROM test_type_record_error1();
+
+
+CREATE FUNCTION test_type_record_error2() RETURNS type_record AS $$
+ return [ 'first' ]
+$$ LANGUAGE plpython3u;
+
+SELECT * FROM test_type_record_error2();
+
+
+CREATE FUNCTION test_type_record_error3() RETURNS type_record AS $$
+ class type_record: pass
+ type_record.first = 'first'
+ return type_record
+$$ LANGUAGE plpython3u;
+
+SELECT * FROM test_type_record_error3();
+
+CREATE FUNCTION test_type_record_error4() RETURNS type_record AS $$
+ return 'foo'
+$$ LANGUAGE plpython3u;
+
+SELECT * FROM test_type_record_error4();
diff --git a/src/pl/plpython/sql/plpython_schema.sql b/src/pl/plpython/sql/plpython_schema.sql
new file mode 100644
index 0000000..a5bdbda
--- /dev/null
+++ b/src/pl/plpython/sql/plpython_schema.sql
@@ -0,0 +1,39 @@
+CREATE TABLE users (
+ fname text not null,
+ lname text not null,
+ username text,
+ userid serial,
+ PRIMARY KEY(lname, fname)
+ ) ;
+
+CREATE INDEX users_username_idx ON users(username);
+CREATE INDEX users_fname_idx ON users(fname);
+CREATE INDEX users_lname_idx ON users(lname);
+CREATE INDEX users_userid_idx ON users(userid);
+
+
+CREATE TABLE taxonomy (
+ id serial primary key,
+ name text unique
+ ) ;
+
+CREATE TABLE entry (
+ accession text not null primary key,
+ eid serial unique,
+ txid int2 not null references taxonomy(id)
+ ) ;
+
+CREATE TABLE sequences (
+ eid int4 not null references entry(eid),
+ pid serial primary key,
+ product text not null,
+ sequence text not null,
+ multipart bool default 'false'
+ ) ;
+CREATE INDEX sequences_product_idx ON sequences(product) ;
+
+CREATE TABLE xsequences (
+ pid int4 not null references sequences(pid),
+ sequence text not null
+ ) ;
+CREATE INDEX xsequences_pid_idx ON xsequences(pid) ;
diff --git a/src/pl/plpython/sql/plpython_setof.sql b/src/pl/plpython/sql/plpython_setof.sql
new file mode 100644
index 0000000..4cfb101
--- /dev/null
+++ b/src/pl/plpython/sql/plpython_setof.sql
@@ -0,0 +1,97 @@
+--
+-- Test returning SETOF
+--
+
+CREATE FUNCTION test_setof_error() RETURNS SETOF text AS $$
+return 37
+$$ LANGUAGE plpython3u;
+
+SELECT test_setof_error();
+
+
+CREATE FUNCTION test_setof_as_list(count integer, content text) RETURNS SETOF text AS $$
+return [ content ]*count
+$$ LANGUAGE plpython3u;
+
+CREATE FUNCTION test_setof_as_tuple(count integer, content text) RETURNS SETOF text AS $$
+t = ()
+for i in range(count):
+ t += ( content, )
+return t
+$$ LANGUAGE plpython3u;
+
+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 plpython3u;
+
+CREATE FUNCTION test_setof_spi_in_iterator() RETURNS SETOF text AS
+$$
+ for s in ('Hello', 'Brave', 'New', 'World'):
+ plpy.execute('select 1')
+ yield s
+ plpy.execute('select 2')
+$$
+LANGUAGE plpython3u;
+
+
+-- 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);
+
+SELECT test_setof_spi_in_iterator();
+
+-- set-returning function that modifies its parameters
+CREATE OR REPLACE FUNCTION ugly(x int, lim int) RETURNS SETOF int AS $$
+global x
+while x <= lim:
+ yield x
+ x = x + 1
+$$ LANGUAGE plpython3u;
+
+SELECT ugly(1, 5);
+
+-- interleaved execution of such a function
+SELECT ugly(1,3), ugly(7,8);
+
+-- returns set of named-composite-type tuples
+CREATE OR REPLACE FUNCTION get_user_records()
+RETURNS SETOF users
+AS $$
+ return plpy.execute("SELECT * FROM users ORDER BY username")
+$$ LANGUAGE plpython3u;
+
+SELECT get_user_records();
+SELECT * FROM get_user_records();
+
+-- same, but returning set of RECORD
+CREATE OR REPLACE FUNCTION get_user_records2()
+RETURNS TABLE(fname text, lname text, username text, userid int)
+AS $$
+ return plpy.execute("SELECT * FROM users ORDER BY username")
+$$ LANGUAGE plpython3u;
+
+SELECT get_user_records2();
+SELECT * FROM get_user_records2();
diff --git a/src/pl/plpython/sql/plpython_spi.sql b/src/pl/plpython/sql/plpython_spi.sql
new file mode 100644
index 0000000..fcd113a
--- /dev/null
+++ b/src/pl/plpython/sql/plpython_spi.sql
@@ -0,0 +1,322 @@
+--
+-- nested calls
+--
+
+CREATE FUNCTION nested_call_one(a text) RETURNS text
+ AS
+'q = "SELECT nested_call_two(''%s'')" % a
+r = plpy.execute(q)
+return r[0]'
+ LANGUAGE plpython3u ;
+
+CREATE FUNCTION nested_call_two(a text) RETURNS text
+ AS
+'q = "SELECT nested_call_three(''%s'')" % a
+r = plpy.execute(q)
+return r[0]'
+ LANGUAGE plpython3u ;
+
+CREATE FUNCTION nested_call_three(a text) RETURNS text
+ AS
+'return a'
+ LANGUAGE plpython3u ;
+
+-- some spi stuff
+
+CREATE FUNCTION spi_prepared_plan_test_one(a text) RETURNS text
+ AS
+'if "myplan" not in SD:
+ q = "SELECT count(*) FROM users WHERE lname = $1"
+ SD["myplan"] = plpy.prepare(q, [ "text" ])
+try:
+ rv = plpy.execute(SD["myplan"], [a])
+ return "there are " + str(rv[0]["count"]) + " " + str(a) + "s"
+except Exception as ex:
+ plpy.error(str(ex))
+return None
+'
+ LANGUAGE plpython3u;
+
+CREATE FUNCTION spi_prepared_plan_test_two(a text) RETURNS text
+ AS
+'if "myplan" not in SD:
+ q = "SELECT count(*) FROM users WHERE lname = $1"
+ SD["myplan"] = plpy.prepare(q, [ "text" ])
+try:
+ rv = SD["myplan"].execute([a])
+ return "there are " + str(rv[0]["count"]) + " " + str(a) + "s"
+except Exception as ex:
+ plpy.error(str(ex))
+return None
+'
+ LANGUAGE plpython3u;
+
+CREATE FUNCTION spi_prepared_plan_test_nested(a text) RETURNS text
+ AS
+'if "myplan" not in SD:
+ q = "SELECT spi_prepared_plan_test_one(''%s'') as count" % a
+ SD["myplan"] = plpy.prepare(q)
+try:
+ rv = plpy.execute(SD["myplan"])
+ if len(rv):
+ return rv[0]["count"]
+except Exception as ex:
+ plpy.error(str(ex))
+return None
+'
+ LANGUAGE plpython3u;
+
+CREATE FUNCTION join_sequences(s sequences) RETURNS text
+ AS
+'if not s["multipart"]:
+ return s["sequence"]
+q = "SELECT sequence FROM xsequences WHERE pid = ''%s''" % s["pid"]
+rv = plpy.execute(q)
+seq = s["sequence"]
+for r in rv:
+ seq = seq + r["sequence"]
+return seq
+'
+ LANGUAGE plpython3u;
+
+CREATE FUNCTION spi_recursive_sum(a int) RETURNS int
+ AS
+'r = 0
+if a > 1:
+ r = plpy.execute("SELECT spi_recursive_sum(%d) as a" % (a-1))[0]["a"]
+return a + r
+'
+ LANGUAGE plpython3u;
+
+--
+-- spi and nested calls
+--
+select nested_call_one('pass this along');
+select spi_prepared_plan_test_one('doe');
+select spi_prepared_plan_test_two('smith');
+select spi_prepared_plan_test_nested('smith');
+
+SELECT join_sequences(sequences) FROM sequences;
+SELECT join_sequences(sequences) FROM sequences
+ WHERE join_sequences(sequences) ~* '^A';
+SELECT join_sequences(sequences) FROM sequences
+ WHERE join_sequences(sequences) ~* '^B';
+
+SELECT spi_recursive_sum(10);
+
+--
+-- plan and result objects
+--
+
+CREATE FUNCTION result_metadata_test(cmd text) RETURNS int
+AS $$
+plan = plpy.prepare(cmd)
+plpy.info(plan.status()) # not really documented or useful
+result = plpy.execute(plan)
+if result.status() > 0:
+ plpy.info(result.colnames())
+ plpy.info(result.coltypes())
+ plpy.info(result.coltypmods())
+ return result.nrows()
+else:
+ return None
+$$ LANGUAGE plpython3u;
+
+SELECT result_metadata_test($$SELECT 1 AS foo, '11'::text AS bar UNION SELECT 2, '22'$$);
+SELECT result_metadata_test($$CREATE TEMPORARY TABLE foo1 (a int, b text)$$);
+
+CREATE FUNCTION result_nrows_test(cmd text) RETURNS int
+AS $$
+result = plpy.execute(cmd)
+return result.nrows()
+$$ LANGUAGE plpython3u;
+
+SELECT result_nrows_test($$SELECT 1$$);
+SELECT result_nrows_test($$CREATE TEMPORARY TABLE foo2 (a int, b text)$$);
+SELECT result_nrows_test($$INSERT INTO foo2 VALUES (1, 'one'), (2, 'two')$$);
+SELECT result_nrows_test($$UPDATE foo2 SET b = '' WHERE a = 2$$);
+
+CREATE FUNCTION result_len_test(cmd text) RETURNS int
+AS $$
+result = plpy.execute(cmd)
+return len(result)
+$$ LANGUAGE plpython3u;
+
+SELECT result_len_test($$SELECT 1$$);
+SELECT result_len_test($$CREATE TEMPORARY TABLE foo3 (a int, b text)$$);
+SELECT result_len_test($$INSERT INTO foo3 VALUES (1, 'one'), (2, 'two')$$);
+SELECT result_len_test($$UPDATE foo3 SET b= '' WHERE a = 2$$);
+
+CREATE FUNCTION result_subscript_test() RETURNS void
+AS $$
+result = plpy.execute("SELECT 1 AS c UNION ALL SELECT 2 "
+ "UNION ALL SELECT 3 UNION ALL SELECT 4")
+
+plpy.info(result[1]['c'])
+plpy.info(result[-1]['c'])
+
+plpy.info([item['c'] for item in result[1:3]])
+plpy.info([item['c'] for item in result[::2]])
+
+result[-1] = {'c': 1000}
+result[:2] = [{'c': 10}, {'c': 100}]
+plpy.info([item['c'] for item in result[:]])
+
+# raises TypeError, catch so further tests could be added
+try:
+ plpy.info(result['foo'])
+except TypeError:
+ pass
+else:
+ assert False, "TypeError not raised"
+
+$$ LANGUAGE plpython3u;
+
+SELECT result_subscript_test();
+
+CREATE FUNCTION result_empty_test() RETURNS void
+AS $$
+result = plpy.execute("select 1 where false")
+
+plpy.info(result[:])
+
+$$ LANGUAGE plpython3u;
+
+SELECT result_empty_test();
+
+CREATE FUNCTION result_str_test(cmd text) RETURNS text
+AS $$
+plan = plpy.prepare(cmd)
+result = plpy.execute(plan)
+return str(result)
+$$ LANGUAGE plpython3u;
+
+SELECT result_str_test($$SELECT 1 AS foo UNION SELECT 2$$);
+SELECT result_str_test($$CREATE TEMPORARY TABLE foo1 (a int, b text)$$);
+
+-- cursor objects
+
+CREATE FUNCTION simple_cursor_test() RETURNS int AS $$
+res = plpy.cursor("select fname, lname from users")
+does = 0
+for row in res:
+ if row['lname'] == 'doe':
+ does += 1
+return does
+$$ LANGUAGE plpython3u;
+
+CREATE FUNCTION double_cursor_close() RETURNS int AS $$
+res = plpy.cursor("select fname, lname from users")
+res.close()
+res.close()
+$$ LANGUAGE plpython3u;
+
+CREATE FUNCTION cursor_fetch() RETURNS int AS $$
+res = plpy.cursor("select fname, lname from users")
+assert len(res.fetch(3)) == 3
+assert len(res.fetch(3)) == 1
+assert len(res.fetch(3)) == 0
+assert len(res.fetch(3)) == 0
+try:
+ # use next() or __next__(), the method name changed in
+ # http://www.python.org/dev/peps/pep-3114/
+ try:
+ res.next()
+ except AttributeError:
+ res.__next__()
+except StopIteration:
+ pass
+else:
+ assert False, "StopIteration not raised"
+$$ LANGUAGE plpython3u;
+
+CREATE FUNCTION cursor_mix_next_and_fetch() RETURNS int AS $$
+res = plpy.cursor("select fname, lname from users order by fname")
+assert len(res.fetch(2)) == 2
+
+item = None
+try:
+ item = res.next()
+except AttributeError:
+ item = res.__next__()
+assert item['fname'] == 'rick'
+
+assert len(res.fetch(2)) == 1
+$$ LANGUAGE plpython3u;
+
+CREATE FUNCTION fetch_after_close() RETURNS int AS $$
+res = plpy.cursor("select fname, lname from users")
+res.close()
+try:
+ res.fetch(1)
+except ValueError:
+ pass
+else:
+ assert False, "ValueError not raised"
+$$ LANGUAGE plpython3u;
+
+CREATE FUNCTION next_after_close() RETURNS int AS $$
+res = plpy.cursor("select fname, lname from users")
+res.close()
+try:
+ try:
+ res.next()
+ except AttributeError:
+ res.__next__()
+except ValueError:
+ pass
+else:
+ assert False, "ValueError not raised"
+$$ LANGUAGE plpython3u;
+
+CREATE FUNCTION cursor_fetch_next_empty() RETURNS int AS $$
+res = plpy.cursor("select fname, lname from users where false")
+assert len(res.fetch(1)) == 0
+try:
+ try:
+ res.next()
+ except AttributeError:
+ res.__next__()
+except StopIteration:
+ pass
+else:
+ assert False, "StopIteration not raised"
+$$ LANGUAGE plpython3u;
+
+CREATE FUNCTION cursor_plan() RETURNS SETOF text AS $$
+plan = plpy.prepare(
+ "select fname, lname from users where fname like $1 || '%' order by fname",
+ ["text"])
+for row in plpy.cursor(plan, ["w"]):
+ yield row['fname']
+for row in plan.cursor(["j"]):
+ yield row['fname']
+$$ LANGUAGE plpython3u;
+
+CREATE FUNCTION cursor_plan_wrong_args() RETURNS SETOF text AS $$
+plan = plpy.prepare("select fname, lname from users where fname like $1 || '%'",
+ ["text"])
+c = plpy.cursor(plan, ["a", "b"])
+$$ LANGUAGE plpython3u;
+
+CREATE TYPE test_composite_type AS (
+ a1 int,
+ a2 varchar
+);
+
+CREATE OR REPLACE FUNCTION plan_composite_args() RETURNS test_composite_type AS $$
+plan = plpy.prepare("select $1 as c1", ["test_composite_type"])
+res = plpy.execute(plan, [{"a1": 3, "a2": "label"}])
+return res[0]["c1"]
+$$ LANGUAGE plpython3u;
+
+SELECT simple_cursor_test();
+SELECT double_cursor_close();
+SELECT cursor_fetch();
+SELECT cursor_mix_next_and_fetch();
+SELECT fetch_after_close();
+SELECT next_after_close();
+SELECT cursor_fetch_next_empty();
+SELECT cursor_plan();
+SELECT cursor_plan_wrong_args();
+SELECT plan_composite_args();
diff --git a/src/pl/plpython/sql/plpython_subtransaction.sql b/src/pl/plpython/sql/plpython_subtransaction.sql
new file mode 100644
index 0000000..c65c380
--- /dev/null
+++ b/src/pl/plpython/sql/plpython_subtransaction.sql
@@ -0,0 +1,262 @@
+--
+-- Test explicit subtransactions
+--
+
+-- Test table to see if transactions get properly rolled back
+
+CREATE TABLE subtransaction_tbl (
+ i integer
+);
+
+CREATE FUNCTION subtransaction_ctx_test(what_error text = NULL) RETURNS text
+AS $$
+with plpy.subtransaction():
+ plpy.execute("INSERT INTO subtransaction_tbl VALUES (1)")
+ plpy.execute("INSERT INTO subtransaction_tbl VALUES (2)")
+ if what_error == "SPI":
+ plpy.execute("INSERT INTO subtransaction_tbl VALUES ('oops')")
+ elif what_error == "Python":
+ raise Exception("Python exception")
+$$ LANGUAGE plpython3u;
+
+SELECT subtransaction_ctx_test();
+SELECT * FROM subtransaction_tbl;
+TRUNCATE subtransaction_tbl;
+SELECT subtransaction_ctx_test('SPI');
+SELECT * FROM subtransaction_tbl;
+TRUNCATE subtransaction_tbl;
+SELECT subtransaction_ctx_test('Python');
+SELECT * FROM subtransaction_tbl;
+TRUNCATE subtransaction_tbl;
+
+-- Nested subtransactions
+
+CREATE FUNCTION subtransaction_nested_test(swallow boolean = 'f') RETURNS text
+AS $$
+plpy.execute("INSERT INTO subtransaction_tbl VALUES (1)")
+with plpy.subtransaction():
+ plpy.execute("INSERT INTO subtransaction_tbl VALUES (2)")
+ try:
+ with plpy.subtransaction():
+ plpy.execute("INSERT INTO subtransaction_tbl VALUES (3)")
+ plpy.execute("error")
+ except plpy.SPIError as e:
+ if not swallow:
+ raise
+ plpy.notice("Swallowed %s(%r)" % (e.__class__.__name__, e.args[0]))
+return "ok"
+$$ LANGUAGE plpython3u;
+
+SELECT subtransaction_nested_test();
+SELECT * FROM subtransaction_tbl;
+TRUNCATE subtransaction_tbl;
+
+SELECT subtransaction_nested_test('t');
+SELECT * FROM subtransaction_tbl;
+TRUNCATE subtransaction_tbl;
+
+-- Nested subtransactions that recursively call code dealing with
+-- subtransactions
+
+CREATE FUNCTION subtransaction_deeply_nested_test() RETURNS text
+AS $$
+plpy.execute("INSERT INTO subtransaction_tbl VALUES (1)")
+with plpy.subtransaction():
+ plpy.execute("INSERT INTO subtransaction_tbl VALUES (2)")
+ plpy.execute("SELECT subtransaction_nested_test('t')")
+return "ok"
+$$ LANGUAGE plpython3u;
+
+SELECT subtransaction_deeply_nested_test();
+SELECT * FROM subtransaction_tbl;
+TRUNCATE subtransaction_tbl;
+
+-- Error conditions from not opening/closing subtransactions
+
+CREATE FUNCTION subtransaction_exit_without_enter() RETURNS void
+AS $$
+plpy.subtransaction().__exit__(None, None, None)
+$$ LANGUAGE plpython3u;
+
+CREATE FUNCTION subtransaction_enter_without_exit() RETURNS void
+AS $$
+plpy.subtransaction().__enter__()
+$$ LANGUAGE plpython3u;
+
+CREATE FUNCTION subtransaction_exit_twice() RETURNS void
+AS $$
+plpy.subtransaction().__enter__()
+plpy.subtransaction().__exit__(None, None, None)
+plpy.subtransaction().__exit__(None, None, None)
+$$ LANGUAGE plpython3u;
+
+CREATE FUNCTION subtransaction_enter_twice() RETURNS void
+AS $$
+plpy.subtransaction().__enter__()
+plpy.subtransaction().__enter__()
+$$ LANGUAGE plpython3u;
+
+CREATE FUNCTION subtransaction_exit_same_subtransaction_twice() RETURNS void
+AS $$
+s = plpy.subtransaction()
+s.__enter__()
+s.__exit__(None, None, None)
+s.__exit__(None, None, None)
+$$ LANGUAGE plpython3u;
+
+CREATE FUNCTION subtransaction_enter_same_subtransaction_twice() RETURNS void
+AS $$
+s = plpy.subtransaction()
+s.__enter__()
+s.__enter__()
+s.__exit__(None, None, None)
+$$ LANGUAGE plpython3u;
+
+-- No warnings here, as the subtransaction gets indeed closed
+CREATE FUNCTION subtransaction_enter_subtransaction_in_with() RETURNS void
+AS $$
+with plpy.subtransaction() as s:
+ s.__enter__()
+$$ LANGUAGE plpython3u;
+
+CREATE FUNCTION subtransaction_exit_subtransaction_in_with() RETURNS void
+AS $$
+try:
+ with plpy.subtransaction() as s:
+ s.__exit__(None, None, None)
+except ValueError as e:
+ raise ValueError(e)
+$$ LANGUAGE plpython3u;
+
+SELECT subtransaction_exit_without_enter();
+SELECT subtransaction_enter_without_exit();
+SELECT subtransaction_exit_twice();
+SELECT subtransaction_enter_twice();
+SELECT subtransaction_exit_same_subtransaction_twice();
+SELECT subtransaction_enter_same_subtransaction_twice();
+SELECT subtransaction_enter_subtransaction_in_with();
+SELECT subtransaction_exit_subtransaction_in_with();
+
+-- Make sure we don't get a "current transaction is aborted" error
+SELECT 1 as test;
+
+-- Mix explicit subtransactions and normal SPI calls
+
+CREATE FUNCTION subtransaction_mix_explicit_and_implicit() RETURNS void
+AS $$
+p = plpy.prepare("INSERT INTO subtransaction_tbl VALUES ($1)", ["integer"])
+try:
+ with plpy.subtransaction():
+ plpy.execute("INSERT INTO subtransaction_tbl VALUES (1)")
+ plpy.execute(p, [2])
+ plpy.execute(p, ["wrong"])
+except plpy.SPIError:
+ plpy.warning("Caught a SPI error from an explicit subtransaction")
+
+try:
+ plpy.execute("INSERT INTO subtransaction_tbl VALUES (1)")
+ plpy.execute(p, [2])
+ plpy.execute(p, ["wrong"])
+except plpy.SPIError:
+ plpy.warning("Caught a SPI error")
+$$ LANGUAGE plpython3u;
+
+SELECT subtransaction_mix_explicit_and_implicit();
+SELECT * FROM subtransaction_tbl;
+TRUNCATE subtransaction_tbl;
+
+-- Alternative method names for Python <2.6
+
+CREATE FUNCTION subtransaction_alternative_names() RETURNS void
+AS $$
+s = plpy.subtransaction()
+s.enter()
+s.exit(None, None, None)
+$$ LANGUAGE plpython3u;
+
+SELECT subtransaction_alternative_names();
+
+-- try/catch inside a subtransaction block
+
+CREATE FUNCTION try_catch_inside_subtransaction() RETURNS void
+AS $$
+with plpy.subtransaction():
+ plpy.execute("INSERT INTO subtransaction_tbl VALUES (1)")
+ try:
+ plpy.execute("INSERT INTO subtransaction_tbl VALUES ('a')")
+ except plpy.SPIError:
+ plpy.notice("caught")
+$$ LANGUAGE plpython3u;
+
+SELECT try_catch_inside_subtransaction();
+SELECT * FROM subtransaction_tbl;
+TRUNCATE subtransaction_tbl;
+
+ALTER TABLE subtransaction_tbl ADD PRIMARY KEY (i);
+
+CREATE FUNCTION pk_violation_inside_subtransaction() RETURNS void
+AS $$
+with plpy.subtransaction():
+ plpy.execute("INSERT INTO subtransaction_tbl VALUES (1)")
+ try:
+ plpy.execute("INSERT INTO subtransaction_tbl VALUES (1)")
+ except plpy.SPIError:
+ plpy.notice("caught")
+$$ LANGUAGE plpython3u;
+
+SELECT pk_violation_inside_subtransaction();
+SELECT * FROM subtransaction_tbl;
+
+DROP TABLE subtransaction_tbl;
+
+-- cursor/subtransactions interactions
+
+CREATE FUNCTION cursor_in_subxact() RETURNS int AS $$
+with plpy.subtransaction():
+ cur = plpy.cursor("select * from generate_series(1, 20) as gen(i)")
+ cur.fetch(10)
+fetched = cur.fetch(10);
+return int(fetched[5]["i"])
+$$ LANGUAGE plpython3u;
+
+CREATE FUNCTION cursor_aborted_subxact() RETURNS int AS $$
+try:
+ with plpy.subtransaction():
+ cur = plpy.cursor("select * from generate_series(1, 20) as gen(i)")
+ cur.fetch(10);
+ plpy.execute("select no_such_function()")
+except plpy.SPIError:
+ fetched = cur.fetch(10)
+ return int(fetched[5]["i"])
+return 0 # not reached
+$$ LANGUAGE plpython3u;
+
+CREATE FUNCTION cursor_plan_aborted_subxact() RETURNS int AS $$
+try:
+ with plpy.subtransaction():
+ plpy.execute('create temporary table tmp(i) '
+ 'as select generate_series(1, 10)')
+ plan = plpy.prepare("select i from tmp")
+ cur = plpy.cursor(plan)
+ plpy.execute("select no_such_function()")
+except plpy.SPIError:
+ fetched = cur.fetch(5)
+ return fetched[2]["i"]
+return 0 # not reached
+$$ LANGUAGE plpython3u;
+
+CREATE FUNCTION cursor_close_aborted_subxact() RETURNS boolean AS $$
+try:
+ with plpy.subtransaction():
+ cur = plpy.cursor('select 1')
+ plpy.execute("select no_such_function()")
+except plpy.SPIError:
+ cur.close()
+ return True
+return False # not reached
+$$ LANGUAGE plpython3u;
+
+SELECT cursor_in_subxact();
+SELECT cursor_aborted_subxact();
+SELECT cursor_plan_aborted_subxact();
+SELECT cursor_close_aborted_subxact();
diff --git a/src/pl/plpython/sql/plpython_test.sql b/src/pl/plpython/sql/plpython_test.sql
new file mode 100644
index 0000000..aa22a27
--- /dev/null
+++ b/src/pl/plpython/sql/plpython_test.sql
@@ -0,0 +1,52 @@
+-- first some tests of basic functionality
+CREATE EXTENSION plpython3u;
+
+-- really stupid function just to get the module loaded
+CREATE FUNCTION stupid() RETURNS text AS 'return "zarkon"' LANGUAGE plpython3u;
+
+select stupid();
+
+-- check 2/3 versioning
+CREATE FUNCTION stupidn() RETURNS text AS 'return "zarkon"' LANGUAGE plpython3u;
+
+select stupidn();
+
+-- test multiple arguments and odd characters in function name
+CREATE FUNCTION "Argument test #1"(u users, a1 text, a2 text) RETURNS text
+ AS
+'keys = list(u.keys())
+keys.sort()
+out = []
+for key in keys:
+ out.append("%s: %s" % (key, u[key]))
+words = a1 + " " + a2 + " => {" + ", ".join(out) + "}"
+return words'
+ LANGUAGE plpython3u;
+
+select "Argument test #1"(users, fname, lname) from users where lname = 'doe' order by 1;
+
+
+-- check module contents
+CREATE FUNCTION module_contents() RETURNS SETOF text AS
+$$
+contents = list(filter(lambda x: not x.startswith("__"), dir(plpy)))
+contents.sort()
+return contents
+$$ LANGUAGE plpython3u;
+
+select module_contents();
+
+CREATE FUNCTION elog_test_basic() RETURNS void
+AS $$
+plpy.debug('debug')
+plpy.log('log')
+plpy.info('info')
+plpy.info(37)
+plpy.info()
+plpy.info('info', 37, [1, 2, 3])
+plpy.notice('notice')
+plpy.warning('warning')
+plpy.error('error')
+$$ LANGUAGE plpython3u;
+
+SELECT elog_test_basic();
diff --git a/src/pl/plpython/sql/plpython_transaction.sql b/src/pl/plpython/sql/plpython_transaction.sql
new file mode 100644
index 0000000..c939ba7
--- /dev/null
+++ b/src/pl/plpython/sql/plpython_transaction.sql
@@ -0,0 +1,182 @@
+CREATE TABLE test1 (a int, b text);
+
+
+CREATE PROCEDURE transaction_test1()
+LANGUAGE plpython3u
+AS $$
+for i in range(0, 10):
+ plpy.execute("INSERT INTO test1 (a) VALUES (%d)" % i)
+ if i % 2 == 0:
+ plpy.commit()
+ else:
+ plpy.rollback()
+$$;
+
+CALL transaction_test1();
+
+SELECT * FROM test1;
+
+
+TRUNCATE test1;
+
+DO
+LANGUAGE plpython3u
+$$
+for i in range(0, 10):
+ plpy.execute("INSERT INTO test1 (a) VALUES (%d)" % i)
+ if i % 2 == 0:
+ plpy.commit()
+ else:
+ plpy.rollback()
+$$;
+
+SELECT * FROM test1;
+
+
+TRUNCATE test1;
+
+-- not allowed in a function
+CREATE FUNCTION transaction_test2() RETURNS int
+LANGUAGE plpython3u
+AS $$
+for i in range(0, 10):
+ plpy.execute("INSERT INTO test1 (a) VALUES (%d)" % i)
+ if i % 2 == 0:
+ plpy.commit()
+ else:
+ plpy.rollback()
+return 1
+$$;
+
+SELECT transaction_test2();
+
+SELECT * FROM test1;
+
+
+-- also not allowed if procedure is called from a function
+CREATE FUNCTION transaction_test3() RETURNS int
+LANGUAGE plpython3u
+AS $$
+plpy.execute("CALL transaction_test1()")
+return 1
+$$;
+
+SELECT transaction_test3();
+
+SELECT * FROM test1;
+
+
+-- DO block inside function
+CREATE FUNCTION transaction_test4() RETURNS int
+LANGUAGE plpython3u
+AS $$
+plpy.execute("DO LANGUAGE plpython3u $x$ plpy.commit() $x$")
+return 1
+$$;
+
+SELECT transaction_test4();
+
+
+-- commit inside subtransaction (prohibited)
+DO LANGUAGE plpython3u $$
+s = plpy.subtransaction()
+s.enter()
+plpy.commit()
+$$;
+
+
+-- commit inside cursor loop
+CREATE TABLE test2 (x int);
+INSERT INTO test2 VALUES (0), (1), (2), (3), (4);
+
+TRUNCATE test1;
+
+DO LANGUAGE plpython3u $$
+for row in plpy.cursor("SELECT * FROM test2 ORDER BY x"):
+ plpy.execute("INSERT INTO test1 (a) VALUES (%s)" % row['x'])
+ plpy.commit()
+$$;
+
+SELECT * FROM test1;
+
+-- check that this doesn't leak a holdable portal
+SELECT * FROM pg_cursors;
+
+
+-- error in cursor loop with commit
+TRUNCATE test1;
+
+DO LANGUAGE plpython3u $$
+for row in plpy.cursor("SELECT * FROM test2 ORDER BY x"):
+ plpy.execute("INSERT INTO test1 (a) VALUES (12/(%s-2))" % row['x'])
+ plpy.commit()
+$$;
+
+SELECT * FROM test1;
+
+SELECT * FROM pg_cursors;
+
+
+-- rollback inside cursor loop
+TRUNCATE test1;
+
+DO LANGUAGE plpython3u $$
+for row in plpy.cursor("SELECT * FROM test2 ORDER BY x"):
+ plpy.execute("INSERT INTO test1 (a) VALUES (%s)" % row['x'])
+ plpy.rollback()
+$$;
+
+SELECT * FROM test1;
+
+SELECT * FROM pg_cursors;
+
+
+-- first commit then rollback inside cursor loop
+TRUNCATE test1;
+
+DO LANGUAGE plpython3u $$
+for row in plpy.cursor("SELECT * FROM test2 ORDER BY x"):
+ plpy.execute("INSERT INTO test1 (a) VALUES (%s)" % row['x'])
+ if row['x'] % 2 == 0:
+ plpy.commit()
+ else:
+ plpy.rollback()
+$$;
+
+SELECT * FROM test1;
+
+SELECT * FROM pg_cursors;
+
+
+-- check handling of an error during COMMIT
+CREATE TABLE testpk (id int PRIMARY KEY);
+CREATE TABLE testfk(f1 int REFERENCES testpk DEFERRABLE INITIALLY DEFERRED);
+
+DO LANGUAGE plpython3u $$
+# this insert will fail during commit:
+plpy.execute("INSERT INTO testfk VALUES (0)")
+plpy.commit()
+plpy.warning('should not get here')
+$$;
+
+SELECT * FROM testpk;
+SELECT * FROM testfk;
+
+DO LANGUAGE plpython3u $$
+# this insert will fail during commit:
+plpy.execute("INSERT INTO testfk VALUES (0)")
+try:
+ plpy.commit()
+except Exception as e:
+ plpy.info('sqlstate: %s' % (e.sqlstate))
+# these inserts should work:
+plpy.execute("INSERT INTO testpk VALUES (1)")
+plpy.execute("INSERT INTO testfk VALUES (1)")
+$$;
+
+SELECT * FROM testpk;
+SELECT * FROM testfk;
+
+
+DROP TABLE test1;
+DROP TABLE test2;
diff --git a/src/pl/plpython/sql/plpython_trigger.sql b/src/pl/plpython/sql/plpython_trigger.sql
new file mode 100644
index 0000000..e5504b9
--- /dev/null
+++ b/src/pl/plpython/sql/plpython_trigger.sql
@@ -0,0 +1,469 @@
+-- these triggers are dedicated to HPHC of RI who
+-- decided that my kid's name was william not willem, and
+-- vigorously resisted all efforts at correction. they have
+-- since gone bankrupt...
+
+CREATE FUNCTION users_insert() returns trigger
+ AS
+'if TD["new"]["fname"] == None or TD["new"]["lname"] == None:
+ return "SKIP"
+if TD["new"]["username"] == None:
+ TD["new"]["username"] = TD["new"]["fname"][:1] + "_" + TD["new"]["lname"]
+ rv = "MODIFY"
+else:
+ rv = None
+if TD["new"]["fname"] == "william":
+ TD["new"]["fname"] = TD["args"][0]
+ rv = "MODIFY"
+return rv'
+ LANGUAGE plpython3u;
+
+
+CREATE FUNCTION users_update() returns trigger
+ AS
+'if TD["event"] == "UPDATE":
+ if TD["old"]["fname"] != TD["new"]["fname"] and TD["old"]["fname"] == TD["args"][0]:
+ return "SKIP"
+return None'
+ LANGUAGE plpython3u;
+
+
+CREATE FUNCTION users_delete() RETURNS trigger
+ AS
+'if TD["old"]["fname"] == TD["args"][0]:
+ return "SKIP"
+return None'
+ LANGUAGE plpython3u;
+
+
+CREATE TRIGGER users_insert_trig BEFORE INSERT ON users FOR EACH ROW
+ EXECUTE PROCEDURE users_insert ('willem');
+
+CREATE TRIGGER users_update_trig BEFORE UPDATE ON users FOR EACH ROW
+ EXECUTE PROCEDURE users_update ('willem');
+
+CREATE TRIGGER users_delete_trig BEFORE DELETE ON users FOR EACH ROW
+ EXECUTE PROCEDURE users_delete ('willem');
+
+
+-- quick peek at the table
+--
+SELECT * FROM users;
+
+-- should fail
+--
+UPDATE users SET fname = 'william' WHERE fname = 'willem';
+
+-- should modify william to willem and create username
+--
+INSERT INTO users (fname, lname) VALUES ('william', 'smith');
+INSERT INTO users (fname, lname, username) VALUES ('charles', 'darwin', 'beagle');
+
+SELECT * FROM users;
+
+
+-- dump trigger data
+
+CREATE TABLE trigger_test
+ (i int, v text );
+
+CREATE TABLE trigger_test_generated (
+ i int,
+ j int GENERATED ALWAYS AS (i * 2) STORED
+);
+
+CREATE FUNCTION trigger_data() RETURNS trigger LANGUAGE plpython3u AS $$
+
+if 'relid' in TD:
+ TD['relid'] = "bogus:12345"
+
+skeys = list(TD.keys())
+skeys.sort()
+for key in skeys:
+ val = TD[key]
+ if not isinstance(val, dict):
+ plpy.notice("TD[" + key + "] => " + str(val))
+ else:
+ # print dicts the hard way because otherwise the order is implementation-dependent
+ valkeys = list(val.keys())
+ valkeys.sort()
+ plpy.notice("TD[" + key + "] => " + '{' + ', '.join([repr(k) + ': ' + repr(val[k]) for k in valkeys]) + '}')
+
+return None
+
+$$;
+
+CREATE TRIGGER show_trigger_data_trig_before
+BEFORE INSERT OR UPDATE OR DELETE ON trigger_test
+FOR EACH ROW EXECUTE PROCEDURE trigger_data(23,'skidoo');
+
+CREATE TRIGGER show_trigger_data_trig_after
+AFTER INSERT OR UPDATE OR DELETE ON trigger_test
+FOR EACH ROW EXECUTE PROCEDURE trigger_data(23,'skidoo');
+
+CREATE TRIGGER show_trigger_data_trig_stmt
+BEFORE INSERT OR UPDATE OR DELETE OR TRUNCATE ON trigger_test
+FOR EACH STATEMENT EXECUTE PROCEDURE trigger_data(23,'skidoo');
+
+insert into trigger_test values(1,'insert');
+update trigger_test set v = 'update' where i = 1;
+delete from trigger_test;
+truncate table trigger_test;
+
+DROP TRIGGER show_trigger_data_trig_stmt on trigger_test;
+DROP TRIGGER show_trigger_data_trig_before on trigger_test;
+DROP TRIGGER show_trigger_data_trig_after on trigger_test;
+
+CREATE TRIGGER show_trigger_data_trig_before
+BEFORE INSERT OR UPDATE OR DELETE ON trigger_test_generated
+FOR EACH ROW EXECUTE PROCEDURE trigger_data();
+
+CREATE TRIGGER show_trigger_data_trig_after
+AFTER INSERT OR UPDATE OR DELETE ON trigger_test_generated
+FOR EACH ROW EXECUTE PROCEDURE trigger_data();
+
+insert into trigger_test_generated (i) values (1);
+update trigger_test_generated set i = 11 where i = 1;
+delete from trigger_test_generated;
+
+DROP TRIGGER show_trigger_data_trig_before ON trigger_test_generated;
+DROP TRIGGER show_trigger_data_trig_after ON trigger_test_generated;
+
+insert into trigger_test values(1,'insert');
+CREATE VIEW trigger_test_view AS SELECT * FROM trigger_test;
+
+CREATE TRIGGER show_trigger_data_trig
+INSTEAD OF INSERT OR UPDATE OR DELETE ON trigger_test_view
+FOR EACH ROW EXECUTE PROCEDURE trigger_data(24,'skidoo view');
+
+insert into trigger_test_view values(2,'insert');
+update trigger_test_view set v = 'update' where i = 1;
+delete from trigger_test_view;
+
+DROP FUNCTION trigger_data() CASCADE;
+DROP VIEW trigger_test_view;
+delete from trigger_test;
+
+
+--
+-- trigger error handling
+--
+
+INSERT INTO trigger_test VALUES (0, 'zero');
+
+
+-- returning non-string from trigger function
+
+CREATE FUNCTION stupid1() RETURNS trigger
+AS $$
+ return 37
+$$ LANGUAGE plpython3u;
+
+CREATE TRIGGER stupid_trigger1
+BEFORE INSERT ON trigger_test
+FOR EACH ROW EXECUTE PROCEDURE stupid1();
+
+INSERT INTO trigger_test VALUES (1, 'one');
+
+DROP TRIGGER stupid_trigger1 ON trigger_test;
+
+
+-- returning MODIFY from DELETE trigger
+
+CREATE FUNCTION stupid2() RETURNS trigger
+AS $$
+ return "MODIFY"
+$$ LANGUAGE plpython3u;
+
+CREATE TRIGGER stupid_trigger2
+BEFORE DELETE ON trigger_test
+FOR EACH ROW EXECUTE PROCEDURE stupid2();
+
+DELETE FROM trigger_test WHERE i = 0;
+
+DROP TRIGGER stupid_trigger2 ON trigger_test;
+
+INSERT INTO trigger_test VALUES (0, 'zero');
+
+
+-- returning unrecognized string from trigger function
+
+CREATE FUNCTION stupid3() RETURNS trigger
+AS $$
+ return "foo"
+$$ LANGUAGE plpython3u;
+
+CREATE TRIGGER stupid_trigger3
+BEFORE UPDATE ON trigger_test
+FOR EACH ROW EXECUTE PROCEDURE stupid3();
+
+UPDATE trigger_test SET v = 'null' WHERE i = 0;
+
+DROP TRIGGER stupid_trigger3 ON trigger_test;
+
+
+-- Unicode variant
+
+CREATE FUNCTION stupid3u() RETURNS trigger
+AS $$
+ return "foo"
+$$ LANGUAGE plpython3u;
+
+CREATE TRIGGER stupid_trigger3
+BEFORE UPDATE ON trigger_test
+FOR EACH ROW EXECUTE PROCEDURE stupid3u();
+
+UPDATE trigger_test SET v = 'null' WHERE i = 0;
+
+DROP TRIGGER stupid_trigger3 ON trigger_test;
+
+
+-- deleting the TD dictionary
+
+CREATE FUNCTION stupid4() RETURNS trigger
+AS $$
+ del TD["new"]
+ return "MODIFY";
+$$ LANGUAGE plpython3u;
+
+CREATE TRIGGER stupid_trigger4
+BEFORE UPDATE ON trigger_test
+FOR EACH ROW EXECUTE PROCEDURE stupid4();
+
+UPDATE trigger_test SET v = 'null' WHERE i = 0;
+
+DROP TRIGGER stupid_trigger4 ON trigger_test;
+
+
+-- TD not a dictionary
+
+CREATE FUNCTION stupid5() RETURNS trigger
+AS $$
+ TD["new"] = ['foo', 'bar']
+ return "MODIFY";
+$$ LANGUAGE plpython3u;
+
+CREATE TRIGGER stupid_trigger5
+BEFORE UPDATE ON trigger_test
+FOR EACH ROW EXECUTE PROCEDURE stupid5();
+
+UPDATE trigger_test SET v = 'null' WHERE i = 0;
+
+DROP TRIGGER stupid_trigger5 ON trigger_test;
+
+
+-- TD not having string keys
+
+CREATE FUNCTION stupid6() RETURNS trigger
+AS $$
+ TD["new"] = {1: 'foo', 2: 'bar'}
+ return "MODIFY";
+$$ LANGUAGE plpython3u;
+
+CREATE TRIGGER stupid_trigger6
+BEFORE UPDATE ON trigger_test
+FOR EACH ROW EXECUTE PROCEDURE stupid6();
+
+UPDATE trigger_test SET v = 'null' WHERE i = 0;
+
+DROP TRIGGER stupid_trigger6 ON trigger_test;
+
+
+-- TD keys not corresponding to row columns
+
+CREATE FUNCTION stupid7() RETURNS trigger
+AS $$
+ TD["new"] = {'v': 'foo', 'a': 'bar'}
+ return "MODIFY";
+$$ LANGUAGE plpython3u;
+
+CREATE TRIGGER stupid_trigger7
+BEFORE UPDATE ON trigger_test
+FOR EACH ROW EXECUTE PROCEDURE stupid7();
+
+UPDATE trigger_test SET v = 'null' WHERE i = 0;
+
+DROP TRIGGER stupid_trigger7 ON trigger_test;
+
+
+-- Unicode variant
+
+CREATE FUNCTION stupid7u() RETURNS trigger
+AS $$
+ TD["new"] = {'v': 'foo', 'a': 'bar'}
+ return "MODIFY"
+$$ LANGUAGE plpython3u;
+
+CREATE TRIGGER stupid_trigger7
+BEFORE UPDATE ON trigger_test
+FOR EACH ROW EXECUTE PROCEDURE stupid7u();
+
+UPDATE trigger_test SET v = 'null' WHERE i = 0;
+
+DROP TRIGGER stupid_trigger7 ON trigger_test;
+
+
+-- calling a trigger function directly
+
+SELECT stupid7();
+
+
+--
+-- Null values
+--
+
+SELECT * FROM trigger_test;
+
+CREATE FUNCTION test_null() RETURNS trigger
+AS $$
+ TD["new"]['v'] = None
+ return "MODIFY"
+$$ LANGUAGE plpython3u;
+
+CREATE TRIGGER test_null_trigger
+BEFORE UPDATE ON trigger_test
+FOR EACH ROW EXECUTE PROCEDURE test_null();
+
+UPDATE trigger_test SET v = 'null' WHERE i = 0;
+
+DROP TRIGGER test_null_trigger ON trigger_test;
+
+SELECT * FROM trigger_test;
+
+
+--
+-- Test that triggers honor typmod when assigning to tuple fields,
+-- as per an early 9.0 bug report
+--
+
+SET DateStyle = 'ISO';
+
+CREATE FUNCTION set_modif_time() RETURNS trigger AS $$
+ TD['new']['modif_time'] = '2010-10-13 21:57:28.930486'
+ return 'MODIFY'
+$$ LANGUAGE plpython3u;
+
+CREATE TABLE pb (a TEXT, modif_time TIMESTAMP(0) WITHOUT TIME ZONE);
+
+CREATE TRIGGER set_modif_time BEFORE UPDATE ON pb
+ FOR EACH ROW EXECUTE PROCEDURE set_modif_time();
+
+INSERT INTO pb VALUES ('a', '2010-10-09 21:57:33.930486');
+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 plpython3u;
+
+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;
+
+
+-- triggers with composite type columns (bug #6559)
+
+CREATE TABLE composite_trigger_noop_test (f1 comp1, f2 comp2);
+
+CREATE FUNCTION composite_trigger_noop_f() RETURNS trigger AS $$
+ return 'MODIFY'
+$$ LANGUAGE plpython3u;
+
+CREATE TRIGGER composite_trigger_noop BEFORE INSERT ON composite_trigger_noop_test
+ FOR EACH ROW EXECUTE PROCEDURE composite_trigger_noop_f();
+
+INSERT INTO composite_trigger_noop_test VALUES (NULL, NULL);
+INSERT INTO composite_trigger_noop_test VALUES (ROW(1, 'f'), NULL);
+INSERT INTO composite_trigger_noop_test VALUES (ROW(NULL, 't'), ROW(1, 'f'));
+SELECT * FROM composite_trigger_noop_test;
+
+
+-- nested composite types
+
+CREATE TYPE comp3 AS (c1 comp1, c2 comp2, m integer);
+
+CREATE TABLE composite_trigger_nested_test(c comp3);
+
+CREATE FUNCTION composite_trigger_nested_f() RETURNS trigger AS $$
+ return 'MODIFY'
+$$ LANGUAGE plpython3u;
+
+CREATE TRIGGER composite_trigger_nested BEFORE INSERT ON composite_trigger_nested_test
+ FOR EACH ROW EXECUTE PROCEDURE composite_trigger_nested_f();
+
+INSERT INTO composite_trigger_nested_test VALUES (NULL);
+INSERT INTO composite_trigger_nested_test VALUES (ROW(ROW(1, 'f'), NULL, 3));
+INSERT INTO composite_trigger_nested_test VALUES (ROW(ROW(NULL, 't'), ROW(1, 'f'), NULL));
+SELECT * FROM composite_trigger_nested_test;
+
+-- check that using a function as a trigger over two tables works correctly
+CREATE FUNCTION trig1234() RETURNS trigger LANGUAGE plpython3u AS $$
+ TD["new"]["data"] = '1234'
+ return 'MODIFY'
+$$;
+
+CREATE TABLE a(data text);
+CREATE TABLE b(data int); -- different type conversion
+
+CREATE TRIGGER a_t BEFORE INSERT ON a FOR EACH ROW EXECUTE PROCEDURE trig1234();
+CREATE TRIGGER b_t BEFORE INSERT ON b FOR EACH ROW EXECUTE PROCEDURE trig1234();
+
+INSERT INTO a DEFAULT VALUES;
+SELECT * FROM a;
+DROP TABLE a;
+INSERT INTO b DEFAULT VALUES;
+SELECT * FROM b;
+
+-- check that SQL run in trigger code can see transition tables
+
+CREATE TABLE transition_table_test (id int, name text);
+INSERT INTO transition_table_test VALUES (1, 'a');
+
+CREATE FUNCTION transition_table_test_f() RETURNS trigger LANGUAGE plpython3u AS
+$$
+ rv = plpy.execute("SELECT * FROM old_table")
+ assert(rv.nrows() == 1)
+ plpy.info("old: " + str(rv[0]["id"]) + " -> " + rv[0]["name"])
+ rv = plpy.execute("SELECT * FROM new_table")
+ assert(rv.nrows() == 1)
+ plpy.info("new: " + str(rv[0]["id"]) + " -> " + rv[0]["name"])
+ return None
+$$;
+
+CREATE TRIGGER a_t AFTER UPDATE ON transition_table_test
+ REFERENCING OLD TABLE AS old_table NEW TABLE AS new_table
+ FOR EACH STATEMENT EXECUTE PROCEDURE transition_table_test_f();
+UPDATE transition_table_test SET name = 'b';
+
+DROP TABLE transition_table_test;
+DROP FUNCTION transition_table_test_f();
+
+
+-- dealing with generated columns
+
+CREATE FUNCTION generated_test_func1() RETURNS trigger
+LANGUAGE plpython3u
+AS $$
+TD['new']['j'] = 5 # not allowed
+return 'MODIFY'
+$$;
+
+CREATE TRIGGER generated_test_trigger1 BEFORE INSERT ON trigger_test_generated
+FOR EACH ROW EXECUTE PROCEDURE generated_test_func1();
+
+TRUNCATE trigger_test_generated;
+INSERT INTO trigger_test_generated (i) VALUES (1);
+SELECT * FROM trigger_test_generated;
diff --git a/src/pl/plpython/sql/plpython_types.sql b/src/pl/plpython/sql/plpython_types.sql
new file mode 100644
index 0000000..0985a9c
--- /dev/null
+++ b/src/pl/plpython/sql/plpython_types.sql
@@ -0,0 +1,622 @@
+--
+-- Test data type behavior
+--
+
+--
+-- Base/common types
+--
+
+CREATE FUNCTION test_type_conversion_bool(x bool) RETURNS bool AS $$
+plpy.info(x, type(x))
+return x
+$$ LANGUAGE plpython3u;
+
+SELECT * FROM test_type_conversion_bool(true);
+SELECT * FROM test_type_conversion_bool(false);
+SELECT * FROM test_type_conversion_bool(null);
+
+
+-- test various other ways to express Booleans in Python
+CREATE FUNCTION test_type_conversion_bool_other(n int) RETURNS bool AS $$
+# numbers
+if n == 0:
+ ret = 0
+elif n == 1:
+ ret = 5
+# strings
+elif n == 2:
+ ret = ''
+elif n == 3:
+ ret = 'fa' # true in Python, false in PostgreSQL
+# containers
+elif n == 4:
+ ret = []
+elif n == 5:
+ ret = [0]
+plpy.info(ret, not not ret)
+return ret
+$$ LANGUAGE plpython3u;
+
+SELECT * FROM test_type_conversion_bool_other(0);
+SELECT * FROM test_type_conversion_bool_other(1);
+SELECT * FROM test_type_conversion_bool_other(2);
+SELECT * FROM test_type_conversion_bool_other(3);
+SELECT * FROM test_type_conversion_bool_other(4);
+SELECT * FROM test_type_conversion_bool_other(5);
+
+
+CREATE FUNCTION test_type_conversion_char(x char) RETURNS char AS $$
+plpy.info(x, type(x))
+return x
+$$ LANGUAGE plpython3u;
+
+SELECT * FROM test_type_conversion_char('a');
+SELECT * FROM test_type_conversion_char(null);
+
+
+CREATE FUNCTION test_type_conversion_int2(x int2) RETURNS int2 AS $$
+plpy.info(x, type(x))
+return x
+$$ LANGUAGE plpython3u;
+
+SELECT * FROM test_type_conversion_int2(100::int2);
+SELECT * FROM test_type_conversion_int2(-100::int2);
+SELECT * FROM test_type_conversion_int2(null);
+
+
+CREATE FUNCTION test_type_conversion_int4(x int4) RETURNS int4 AS $$
+plpy.info(x, type(x))
+return x
+$$ LANGUAGE plpython3u;
+
+SELECT * FROM test_type_conversion_int4(100);
+SELECT * FROM test_type_conversion_int4(-100);
+SELECT * FROM test_type_conversion_int4(null);
+
+
+CREATE FUNCTION test_type_conversion_int8(x int8) RETURNS int8 AS $$
+plpy.info(x, type(x))
+return x
+$$ LANGUAGE plpython3u;
+
+SELECT * FROM test_type_conversion_int8(100);
+SELECT * FROM test_type_conversion_int8(-100);
+SELECT * FROM test_type_conversion_int8(5000000000);
+SELECT * FROM test_type_conversion_int8(null);
+
+
+CREATE FUNCTION test_type_conversion_numeric(x numeric) RETURNS numeric AS $$
+# print just the class name, not the type, to avoid differences
+# between decimal and cdecimal
+plpy.info(str(x), x.__class__.__name__)
+return x
+$$ LANGUAGE plpython3u;
+
+SELECT * FROM test_type_conversion_numeric(100);
+SELECT * FROM test_type_conversion_numeric(-100);
+SELECT * FROM test_type_conversion_numeric(100.0);
+SELECT * FROM test_type_conversion_numeric(100.00);
+SELECT * FROM test_type_conversion_numeric(5000000000.5);
+SELECT * FROM test_type_conversion_numeric(1234567890.0987654321);
+SELECT * FROM test_type_conversion_numeric(-1234567890.0987654321);
+SELECT * FROM test_type_conversion_numeric(null);
+
+
+CREATE FUNCTION test_type_conversion_float4(x float4) RETURNS float4 AS $$
+plpy.info(x, type(x))
+return x
+$$ LANGUAGE plpython3u;
+
+SELECT * FROM test_type_conversion_float4(100);
+SELECT * FROM test_type_conversion_float4(-100);
+SELECT * FROM test_type_conversion_float4(5000.5);
+SELECT * FROM test_type_conversion_float4(null);
+
+
+CREATE FUNCTION test_type_conversion_float8(x float8) RETURNS float8 AS $$
+plpy.info(x, type(x))
+return x
+$$ LANGUAGE plpython3u;
+
+SELECT * FROM test_type_conversion_float8(100);
+SELECT * FROM test_type_conversion_float8(-100);
+SELECT * FROM test_type_conversion_float8(5000000000.5);
+SELECT * FROM test_type_conversion_float8(null);
+SELECT * FROM test_type_conversion_float8(100100100.654321);
+
+
+CREATE FUNCTION test_type_conversion_oid(x oid) RETURNS oid AS $$
+plpy.info(x, type(x))
+return x
+$$ LANGUAGE plpython3u;
+
+SELECT * FROM test_type_conversion_oid(100);
+SELECT * FROM test_type_conversion_oid(2147483649);
+SELECT * FROM test_type_conversion_oid(null);
+
+
+CREATE FUNCTION test_type_conversion_text(x text) RETURNS text AS $$
+plpy.info(x, type(x))
+return x
+$$ LANGUAGE plpython3u;
+
+SELECT * FROM test_type_conversion_text('hello world');
+SELECT * FROM test_type_conversion_text(null);
+
+
+CREATE FUNCTION test_type_conversion_bytea(x bytea) RETURNS bytea AS $$
+plpy.info(x, type(x))
+return x
+$$ LANGUAGE plpython3u;
+
+SELECT * FROM test_type_conversion_bytea('hello world');
+SELECT * FROM test_type_conversion_bytea(E'null\\000byte');
+SELECT * FROM test_type_conversion_bytea(null);
+
+
+CREATE FUNCTION test_type_marshal() RETURNS bytea AS $$
+import marshal
+return marshal.dumps('hello world')
+$$ LANGUAGE plpython3u;
+
+CREATE FUNCTION test_type_unmarshal(x bytea) RETURNS text AS $$
+import marshal
+try:
+ return marshal.loads(x)
+except ValueError as e:
+ return 'FAILED: ' + str(e)
+$$ LANGUAGE plpython3u;
+
+SELECT test_type_unmarshal(x) FROM test_type_marshal() x;
+
+
+--
+-- Domains
+--
+
+CREATE DOMAIN booltrue AS bool CHECK (VALUE IS TRUE OR VALUE IS NULL);
+
+CREATE FUNCTION test_type_conversion_booltrue(x booltrue, y bool) RETURNS booltrue AS $$
+return y
+$$ LANGUAGE plpython3u;
+
+SELECT * FROM test_type_conversion_booltrue(true, true);
+SELECT * FROM test_type_conversion_booltrue(false, true);
+SELECT * FROM test_type_conversion_booltrue(true, false);
+
+
+CREATE DOMAIN uint2 AS int2 CHECK (VALUE >= 0);
+
+CREATE FUNCTION test_type_conversion_uint2(x uint2, y int) RETURNS uint2 AS $$
+plpy.info(x, type(x))
+return y
+$$ LANGUAGE plpython3u;
+
+SELECT * FROM test_type_conversion_uint2(100::uint2, 50);
+SELECT * FROM test_type_conversion_uint2(100::uint2, -50);
+SELECT * FROM test_type_conversion_uint2(null, 1);
+
+
+CREATE DOMAIN nnint AS int CHECK (VALUE IS NOT NULL);
+
+CREATE FUNCTION test_type_conversion_nnint(x nnint, y int) RETURNS nnint AS $$
+return y
+$$ LANGUAGE plpython3u;
+
+SELECT * FROM test_type_conversion_nnint(10, 20);
+SELECT * FROM test_type_conversion_nnint(null, 20);
+SELECT * FROM test_type_conversion_nnint(10, null);
+
+
+CREATE DOMAIN bytea10 AS bytea CHECK (octet_length(VALUE) = 10 AND VALUE IS NOT NULL);
+
+CREATE FUNCTION test_type_conversion_bytea10(x bytea10, y bytea) RETURNS bytea10 AS $$
+plpy.info(x, type(x))
+return y
+$$ LANGUAGE plpython3u;
+
+SELECT * FROM test_type_conversion_bytea10('hello wold', 'hello wold');
+SELECT * FROM test_type_conversion_bytea10('hello world', 'hello wold');
+SELECT * FROM test_type_conversion_bytea10('hello word', 'hello world');
+SELECT * FROM test_type_conversion_bytea10(null, 'hello word');
+SELECT * FROM test_type_conversion_bytea10('hello word', null);
+
+
+--
+-- Arrays
+--
+
+CREATE FUNCTION test_type_conversion_array_int4(x int4[]) RETURNS int4[] AS $$
+plpy.info(x, type(x))
+return x
+$$ LANGUAGE plpython3u;
+
+SELECT * FROM test_type_conversion_array_int4(ARRAY[0, 100]);
+SELECT * FROM test_type_conversion_array_int4(ARRAY[0,-100,55]);
+SELECT * FROM test_type_conversion_array_int4(ARRAY[NULL,1]);
+SELECT * FROM test_type_conversion_array_int4(ARRAY[]::integer[]);
+SELECT * FROM test_type_conversion_array_int4(NULL);
+SELECT * FROM test_type_conversion_array_int4(ARRAY[[1,2,3],[4,5,6]]);
+SELECT * FROM test_type_conversion_array_int4(ARRAY[[[1,2,NULL],[NULL,5,6]],[[NULL,8,9],[10,11,12]]]);
+SELECT * FROM test_type_conversion_array_int4('[2:4]={1,2,3}');
+
+CREATE FUNCTION test_type_conversion_array_int8(x int8[]) RETURNS int8[] AS $$
+plpy.info(x, type(x))
+return x
+$$ LANGUAGE plpython3u;
+
+SELECT * FROM test_type_conversion_array_int8(ARRAY[[[1,2,NULL],[NULL,5,6]],[[NULL,8,9],[10,11,12]]]::int8[]);
+
+CREATE FUNCTION test_type_conversion_array_date(x date[]) RETURNS date[] AS $$
+plpy.info(x, type(x))
+return x
+$$ LANGUAGE plpython3u;
+
+SELECT * FROM test_type_conversion_array_date(ARRAY[[['2016-09-21','2016-09-22',NULL],[NULL,'2016-10-21','2016-10-22']],
+ [[NULL,'2016-11-21','2016-10-21'],['2015-09-21','2015-09-22','2014-09-21']]]::date[]);
+
+CREATE FUNCTION test_type_conversion_array_timestamp(x timestamp[]) RETURNS timestamp[] AS $$
+plpy.info(x, type(x))
+return x
+$$ LANGUAGE plpython3u;
+
+SELECT * FROM test_type_conversion_array_timestamp(ARRAY[[['2016-09-21 15:34:24.078792-04','2016-10-22 11:34:24.078795-04',NULL],
+ [NULL,'2016-10-21 11:34:25.078792-04','2016-10-21 11:34:24.098792-04']],
+ [[NULL,'2016-01-21 11:34:24.078792-04','2016-11-21 11:34:24.108792-04'],
+ ['2015-09-21 11:34:24.079792-04','2014-09-21 11:34:24.078792-04','2013-09-21 11:34:24.078792-04']]]::timestamp[]);
+
+
+CREATE OR REPLACE FUNCTION pyreturnmultidemint4(h int4, i int4, j int4, k int4 ) RETURNS int4[] AS $BODY$
+m = [[[[x for x in range(h)] for y in range(i)] for z in range(j)] for w in range(k)]
+plpy.info(m, type(m))
+return m
+$BODY$ LANGUAGE plpython3u;
+
+select pyreturnmultidemint4(8,5,3,2);
+
+CREATE OR REPLACE FUNCTION pyreturnmultidemint8(h int4, i int4, j int4, k int4 ) RETURNS int8[] AS $BODY$
+m = [[[[x for x in range(h)] for y in range(i)] for z in range(j)] for w in range(k)]
+plpy.info(m, type(m))
+return m
+$BODY$ LANGUAGE plpython3u;
+
+select pyreturnmultidemint8(5,5,3,2);
+
+CREATE OR REPLACE FUNCTION pyreturnmultidemfloat4(h int4, i int4, j int4, k int4 ) RETURNS float4[] AS $BODY$
+m = [[[[x for x in range(h)] for y in range(i)] for z in range(j)] for w in range(k)]
+plpy.info(m, type(m))
+return m
+$BODY$ LANGUAGE plpython3u;
+
+select pyreturnmultidemfloat4(6,5,3,2);
+
+CREATE OR REPLACE FUNCTION pyreturnmultidemfloat8(h int4, i int4, j int4, k int4 ) RETURNS float8[] AS $BODY$
+m = [[[[x for x in range(h)] for y in range(i)] for z in range(j)] for w in range(k)]
+plpy.info(m, type(m))
+return m
+$BODY$ LANGUAGE plpython3u;
+
+select pyreturnmultidemfloat8(7,5,3,2);
+
+CREATE FUNCTION test_type_conversion_array_text(x text[]) RETURNS text[] AS $$
+plpy.info(x, type(x))
+return x
+$$ LANGUAGE plpython3u;
+
+SELECT * FROM test_type_conversion_array_text(ARRAY['foo', 'bar']);
+SELECT * FROM test_type_conversion_array_text(ARRAY[['foo', 'bar'],['foo2', 'bar2']]);
+
+
+CREATE FUNCTION test_type_conversion_array_bytea(x bytea[]) RETURNS bytea[] AS $$
+plpy.info(x, type(x))
+return x
+$$ LANGUAGE plpython3u;
+
+SELECT * FROM test_type_conversion_array_bytea(ARRAY[E'\\xdeadbeef'::bytea, NULL]);
+
+
+CREATE FUNCTION test_type_conversion_array_mixed1() RETURNS text[] AS $$
+return [123, 'abc']
+$$ LANGUAGE plpython3u;
+
+SELECT * FROM test_type_conversion_array_mixed1();
+
+
+CREATE FUNCTION test_type_conversion_array_mixed2() RETURNS int[] AS $$
+return [123, 'abc']
+$$ LANGUAGE plpython3u;
+
+SELECT * FROM test_type_conversion_array_mixed2();
+
+
+-- check output of multi-dimensional arrays
+CREATE FUNCTION test_type_conversion_md_array_out() RETURNS text[] AS $$
+return [['a'], ['b'], ['c']]
+$$ LANGUAGE plpython3u;
+
+select test_type_conversion_md_array_out();
+
+CREATE OR REPLACE FUNCTION test_type_conversion_md_array_out() RETURNS text[] AS $$
+return [[], []]
+$$ LANGUAGE plpython3u;
+
+select test_type_conversion_md_array_out();
+
+CREATE OR REPLACE FUNCTION test_type_conversion_md_array_out() RETURNS text[] AS $$
+return [[], [1]]
+$$ LANGUAGE plpython3u;
+
+select test_type_conversion_md_array_out(); -- fail
+
+CREATE OR REPLACE FUNCTION test_type_conversion_md_array_out() RETURNS text[] AS $$
+return [[], 1]
+$$ LANGUAGE plpython3u;
+
+select test_type_conversion_md_array_out(); -- fail
+
+CREATE OR REPLACE FUNCTION test_type_conversion_md_array_out() RETURNS text[] AS $$
+return [1, []]
+$$ LANGUAGE plpython3u;
+
+select test_type_conversion_md_array_out(); -- fail
+
+CREATE OR REPLACE FUNCTION test_type_conversion_md_array_out() RETURNS text[] AS $$
+return [[1], [[]]]
+$$ LANGUAGE plpython3u;
+
+select test_type_conversion_md_array_out(); -- fail
+
+
+CREATE FUNCTION test_type_conversion_mdarray_malformed() RETURNS int[] AS $$
+return [[1,2,3],[4,5]]
+$$ LANGUAGE plpython3u;
+
+SELECT * FROM test_type_conversion_mdarray_malformed();
+
+CREATE FUNCTION test_type_conversion_mdarray_malformed2() RETURNS text[] AS $$
+return [[1,2,3], "abc"]
+$$ LANGUAGE plpython3u;
+
+SELECT * FROM test_type_conversion_mdarray_malformed2();
+
+CREATE FUNCTION test_type_conversion_mdarray_malformed3() RETURNS text[] AS $$
+return ["abc", [1,2,3]]
+$$ LANGUAGE plpython3u;
+
+SELECT * FROM test_type_conversion_mdarray_malformed3();
+
+CREATE FUNCTION test_type_conversion_mdarray_toodeep() RETURNS int[] AS $$
+return [[[[[[[1]]]]]]]
+$$ LANGUAGE plpython3u;
+
+SELECT * FROM test_type_conversion_mdarray_toodeep();
+
+
+CREATE FUNCTION test_type_conversion_array_record() RETURNS type_record[] AS $$
+return [{'first': 'one', 'second': 42}, {'first': 'two', 'second': 11}]
+$$ LANGUAGE plpython3u;
+
+SELECT * FROM test_type_conversion_array_record();
+
+
+CREATE FUNCTION test_type_conversion_array_string() RETURNS text[] AS $$
+return 'abc'
+$$ LANGUAGE plpython3u;
+
+SELECT * FROM test_type_conversion_array_string();
+
+CREATE FUNCTION test_type_conversion_array_tuple() RETURNS text[] AS $$
+return ('abc', 'def')
+$$ LANGUAGE plpython3u;
+
+SELECT * FROM test_type_conversion_array_tuple();
+
+CREATE FUNCTION test_type_conversion_array_error() RETURNS int[] AS $$
+return 5
+$$ LANGUAGE plpython3u;
+
+SELECT * FROM test_type_conversion_array_error();
+
+
+--
+-- Domains over arrays
+--
+
+CREATE DOMAIN ordered_pair_domain AS integer[] CHECK (array_length(VALUE,1)=2 AND VALUE[1] < VALUE[2]);
+
+CREATE FUNCTION test_type_conversion_array_domain(x ordered_pair_domain) RETURNS ordered_pair_domain AS $$
+plpy.info(x, type(x))
+return x
+$$ LANGUAGE plpython3u;
+
+SELECT * FROM test_type_conversion_array_domain(ARRAY[0, 100]::ordered_pair_domain);
+SELECT * FROM test_type_conversion_array_domain(NULL::ordered_pair_domain);
+
+CREATE FUNCTION test_type_conversion_array_domain_check_violation() RETURNS ordered_pair_domain AS $$
+return [2,1]
+$$ LANGUAGE plpython3u;
+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 plpython3u;
+
+select test_read_uint2_array(array[1::uint2]);
+
+CREATE FUNCTION test_build_uint2_array(x int2) RETURNS uint2[] AS $$
+return [x, x]
+$$ LANGUAGE plpython3u;
+
+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 plpython3u;
+
+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 plpython3u;
+
+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 plpython3u;
+
+select test_type_conversion_array_domain_array(array[array[2,4]::ordered_pair_domain]);
+
+
+---
+--- Composite types
+---
+
+CREATE TABLE employee (
+ name text,
+ basesalary integer,
+ bonus integer
+);
+
+INSERT INTO employee VALUES ('John', 100, 10), ('Mary', 200, 10);
+
+CREATE OR REPLACE FUNCTION test_composite_table_input(e employee) RETURNS integer AS $$
+return e['basesalary'] + e['bonus']
+$$ LANGUAGE plpython3u;
+
+SELECT name, test_composite_table_input(employee.*) FROM employee;
+
+ALTER TABLE employee DROP bonus;
+
+SELECT name, test_composite_table_input(employee.*) FROM employee;
+
+ALTER TABLE employee ADD bonus integer;
+UPDATE employee SET bonus = 10;
+
+SELECT name, test_composite_table_input(employee.*) FROM employee;
+
+CREATE TYPE named_pair AS (
+ i integer,
+ j integer
+);
+
+CREATE OR REPLACE FUNCTION test_composite_type_input(p named_pair) RETURNS integer AS $$
+return sum(p.values())
+$$ LANGUAGE plpython3u;
+
+SELECT test_composite_type_input(row(1, 2));
+
+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 plpython3u;
+
+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 plpython3u;
+
+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 plpython3u;
+
+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 plpython3u;
+
+SELECT build_ordered_named_pairs(1,2);
+SELECT build_ordered_named_pairs(2,1); -- fail
+
+
+--
+-- Prepared statements
+--
+
+CREATE OR REPLACE FUNCTION test_prep_bool_input() RETURNS int
+LANGUAGE plpython3u
+AS $$
+plan = plpy.prepare("SELECT CASE WHEN $1 THEN 1 ELSE 0 END AS val", ['boolean'])
+rv = plpy.execute(plan, ['fa'], 5) # 'fa' is true in Python
+return rv[0]['val']
+$$;
+
+SELECT test_prep_bool_input(); -- 1
+
+
+CREATE OR REPLACE FUNCTION test_prep_bool_output() RETURNS bool
+LANGUAGE plpython3u
+AS $$
+plan = plpy.prepare("SELECT $1 = 1 AS val", ['int'])
+rv = plpy.execute(plan, [0], 5)
+plpy.info(rv[0])
+return rv[0]['val']
+$$;
+
+SELECT test_prep_bool_output(); -- false
+
+
+CREATE OR REPLACE FUNCTION test_prep_bytea_input(bb bytea) RETURNS int
+LANGUAGE plpython3u
+AS $$
+plan = plpy.prepare("SELECT octet_length($1) AS val", ['bytea'])
+rv = plpy.execute(plan, [bb], 5)
+return rv[0]['val']
+$$;
+
+SELECT test_prep_bytea_input(E'a\\000b'); -- 3 (embedded null formerly truncated value)
+
+
+CREATE OR REPLACE FUNCTION test_prep_bytea_output() RETURNS bytea
+LANGUAGE plpython3u
+AS $$
+plan = plpy.prepare("SELECT decode('aa00bb', 'hex') AS val")
+rv = plpy.execute(plan, [], 5)
+plpy.info(rv[0])
+return rv[0]['val']
+$$;
+
+SELECT test_prep_bytea_output();
diff --git a/src/pl/plpython/sql/plpython_unicode.sql b/src/pl/plpython/sql/plpython_unicode.sql
new file mode 100644
index 0000000..14f7b4e
--- /dev/null
+++ b/src/pl/plpython/sql/plpython_unicode.sql
@@ -0,0 +1,45 @@
+--
+-- Unicode handling
+--
+-- Note: this test case is known to fail if the database encoding is
+-- EUC_CN, EUC_JP, EUC_KR, or EUC_TW, for lack of any equivalent to
+-- U+00A0 (no-break space) in those encodings. However, testing with
+-- plain ASCII data would be rather useless, so we must live with that.
+--
+
+SET client_encoding TO UTF8;
+
+CREATE TABLE unicode_test (
+ testvalue text NOT NULL
+);
+
+CREATE FUNCTION unicode_return() RETURNS text AS E'
+return "\\xA0"
+' LANGUAGE plpython3u;
+
+CREATE FUNCTION unicode_trigger() RETURNS trigger AS E'
+TD["new"]["testvalue"] = "\\xA0"
+return "MODIFY"
+' LANGUAGE plpython3u;
+
+CREATE TRIGGER unicode_test_bi BEFORE INSERT ON unicode_test
+ FOR EACH ROW EXECUTE PROCEDURE unicode_trigger();
+
+CREATE FUNCTION unicode_plan1() RETURNS text AS E'
+plan = plpy.prepare("SELECT $1 AS testvalue", ["text"])
+rv = plpy.execute(plan, ["\\xA0"], 1)
+return rv[0]["testvalue"]
+' LANGUAGE plpython3u;
+
+CREATE FUNCTION unicode_plan2() RETURNS text AS E'
+plan = plpy.prepare("SELECT $1 || $2 AS testvalue", ["text", "text"])
+rv = plpy.execute(plan, ["foo", "bar"], 1)
+return rv[0]["testvalue"]
+' LANGUAGE plpython3u;
+
+
+SELECT unicode_return();
+INSERT INTO unicode_test (testvalue) VALUES ('test');
+SELECT * FROM unicode_test;
+SELECT unicode_plan1();
+SELECT unicode_plan2();
diff --git a/src/pl/plpython/sql/plpython_void.sql b/src/pl/plpython/sql/plpython_void.sql
new file mode 100644
index 0000000..5a1a671
--- /dev/null
+++ b/src/pl/plpython/sql/plpython_void.sql
@@ -0,0 +1,22 @@
+--
+-- Tests for functions that return void
+--
+
+CREATE FUNCTION test_void_func1() RETURNS void AS $$
+x = 10
+$$ LANGUAGE plpython3u;
+
+-- illegal: can't return non-None value in void-returning func
+CREATE FUNCTION test_void_func2() RETURNS void AS $$
+return 10
+$$ LANGUAGE plpython3u;
+
+CREATE FUNCTION test_return_none() RETURNS int AS $$
+None
+$$ LANGUAGE plpython3u;
+
+
+-- Tests for functions returning void
+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";