summaryrefslogtreecommitdiffstats
path: root/src/pl/plpython
diff options
context:
space:
mode:
Diffstat (limited to 'src/pl/plpython')
-rw-r--r--src/pl/plpython/.gitignore5
-rw-r--r--src/pl/plpython/Makefile157
-rw-r--r--src/pl/plpython/expected/README3
-rw-r--r--src/pl/plpython/expected/plpython_call.out75
-rw-r--r--src/pl/plpython/expected/plpython_composite.out594
-rw-r--r--src/pl/plpython/expected/plpython_do.out8
-rw-r--r--src/pl/plpython/expected/plpython_drop.out5
-rw-r--r--src/pl/plpython/expected/plpython_ereport.out214
-rw-r--r--src/pl/plpython/expected/plpython_error.out460
-rw-r--r--src/pl/plpython/expected/plpython_error_5.out460
-rw-r--r--src/pl/plpython/expected/plpython_global.out52
-rw-r--r--src/pl/plpython/expected/plpython_import.out79
-rw-r--r--src/pl/plpython/expected/plpython_newline.out30
-rw-r--r--src/pl/plpython/expected/plpython_params.out64
-rw-r--r--src/pl/plpython/expected/plpython_populate.out22
-rw-r--r--src/pl/plpython/expected/plpython_quote.out56
-rw-r--r--src/pl/plpython/expected/plpython_record.out373
-rw-r--r--src/pl/plpython/expected/plpython_schema.out33
-rw-r--r--src/pl/plpython/expected/plpython_setof.out200
-rw-r--r--src/pl/plpython/expected/plpython_spi.out466
-rw-r--r--src/pl/plpython/expected/plpython_subtransaction.out401
-rw-r--r--src/pl/plpython/expected/plpython_test.out93
-rw-r--r--src/pl/plpython/expected/plpython_transaction.out250
-rw-r--r--src/pl/plpython/expected/plpython_trigger.out620
-rw-r--r--src/pl/plpython/expected/plpython_types.out1069
-rw-r--r--src/pl/plpython/expected/plpython_unicode.out56
-rw-r--r--src/pl/plpython/expected/plpython_void.out30
-rw-r--r--src/pl/plpython/generate-spiexceptions.pl44
-rw-r--r--src/pl/plpython/meson.build109
-rw-r--r--src/pl/plpython/nls.mk20
-rw-r--r--src/pl/plpython/plpy_cursorobject.c492
-rw-r--r--src/pl/plpython/plpy_cursorobject.h24
-rw-r--r--src/pl/plpython/plpy_elog.c599
-rw-r--r--src/pl/plpython/plpy_elog.h46
-rw-r--r--src/pl/plpython/plpy_exec.c1098
-rw-r--r--src/pl/plpython/plpy_exec.h13
-rw-r--r--src/pl/plpython/plpy_main.c419
-rw-r--r--src/pl/plpython/plpy_main.h31
-rw-r--r--src/pl/plpython/plpy_planobject.c125
-rw-r--r--src/pl/plpython/plpy_planobject.h27
-rw-r--r--src/pl/plpython/plpy_plpymodule.c561
-rw-r--r--src/pl/plpython/plpy_plpymodule.h18
-rw-r--r--src/pl/plpython/plpy_procedure.c470
-rw-r--r--src/pl/plpython/plpy_procedure.h70
-rw-r--r--src/pl/plpython/plpy_resultobject.c250
-rw-r--r--src/pl/plpython/plpy_resultobject.h27
-rw-r--r--src/pl/plpython/plpy_spi.c666
-rw-r--r--src/pl/plpython/plpy_spi.h29
-rw-r--r--src/pl/plpython/plpy_subxactobject.c186
-rw-r--r--src/pl/plpython/plpy_subxactobject.h33
-rw-r--r--src/pl/plpython/plpy_typeio.c1557
-rw-r--r--src/pl/plpython/plpy_typeio.h175
-rw-r--r--src/pl/plpython/plpy_util.c121
-rw-r--r--src/pl/plpython/plpy_util.h17
-rw-r--r--src/pl/plpython/plpython.h37
-rw-r--r--src/pl/plpython/plpython3u--1.0.sql17
-rw-r--r--src/pl/plpython/plpython3u.control7
-rw-r--r--src/pl/plpython/plpython_system.h53
-rw-r--r--src/pl/plpython/po/LINGUAS1
-rw-r--r--src/pl/plpython/po/cs.po503
-rw-r--r--src/pl/plpython/po/de.po451
-rw-r--r--src/pl/plpython/po/el.po461
-rw-r--r--src/pl/plpython/po/es.po454
-rw-r--r--src/pl/plpython/po/fr.po590
-rw-r--r--src/pl/plpython/po/it.po470
-rw-r--r--src/pl/plpython/po/ja.po497
-rw-r--r--src/pl/plpython/po/ka.po462
-rw-r--r--src/pl/plpython/po/ko.po478
-rw-r--r--src/pl/plpython/po/meson.build3
-rw-r--r--src/pl/plpython/po/pl.po507
-rw-r--r--src/pl/plpython/po/pt_BR.po461
-rw-r--r--src/pl/plpython/po/ru.po561
-rw-r--r--src/pl/plpython/po/sv.po461
-rw-r--r--src/pl/plpython/po/tr.po537
-rw-r--r--src/pl/plpython/po/uk.po452
-rw-r--r--src/pl/plpython/po/vi.po485
-rw-r--r--src/pl/plpython/po/zh_CN.po459
-rw-r--r--src/pl/plpython/po/zh_TW.po496
-rw-r--r--src/pl/plpython/spiexceptions.h998
-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.sql357
-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
102 files changed, 26311 insertions, 0 deletions
diff --git a/src/pl/plpython/.gitignore b/src/pl/plpython/.gitignore
new file mode 100644
index 0000000..07bee6a
--- /dev/null
+++ b/src/pl/plpython/.gitignore
@@ -0,0 +1,5 @@
+/spiexceptions.h
+# Generated subdirectories
+/log/
+/results/
+/tmp_check/
diff --git a/src/pl/plpython/Makefile b/src/pl/plpython/Makefile
new file mode 100644
index 0000000..bb26426
--- /dev/null
+++ b/src/pl/plpython/Makefile
@@ -0,0 +1,157 @@
+# src/pl/plpython/Makefile
+
+subdir = src/pl/plpython
+top_builddir = ../../..
+include $(top_builddir)/src/Makefile.global
+
+
+# On Windows we have to remove -lpython from the link since we are
+# building our own
+ifeq ($(PORTNAME), win32)
+override python_libspec =
+endif
+
+override CPPFLAGS := -I. -I$(srcdir) $(python_includespec) $(CPPFLAGS)
+
+rpathdir = $(python_libdir)
+
+PGFILEDESC = "PL/Python - procedural language"
+
+NAME = plpython$(python_majorversion)
+
+OBJS = \
+ $(WIN32RES) \
+ plpy_cursorobject.o \
+ plpy_elog.o \
+ plpy_exec.o \
+ plpy_main.o \
+ plpy_planobject.o \
+ plpy_plpymodule.o \
+ plpy_procedure.o \
+ plpy_resultobject.o \
+ plpy_spi.o \
+ plpy_subxactobject.o \
+ plpy_typeio.o \
+ plpy_util.o
+
+DATA = $(NAME)u.control $(NAME)u--1.0.sql
+
+# header files to install - it's not clear which of these might be needed
+# so install them all.
+INCS = plpython.h \
+ plpython_system.h \
+ plpy_cursorobject.h \
+ plpy_elog.h \
+ plpy_exec.h \
+ plpy_main.h \
+ plpy_planobject.h \
+ plpy_plpymodule.h \
+ plpy_procedure.h \
+ plpy_resultobject.h \
+ plpy_spi.h \
+ plpy_subxactobject.h \
+ plpy_typeio.h \
+ plpy_util.h
+
+# Python on win32 ships with import libraries only for Microsoft Visual C++,
+# which are not compatible with mingw gcc. Therefore we need to build a
+# new import library to link with.
+ifeq ($(PORTNAME), win32)
+
+pytverstr=$(subst .,,${python_version})
+PYTHONDLL=$(subst \,/,$(WINDIR))/system32/python${pytverstr}.dll
+
+OBJS += libpython${pytverstr}.a
+
+libpython${pytverstr}.a: python${pytverstr}.def
+ dlltool --dllname python${pytverstr}.dll --def python${pytverstr}.def --output-lib libpython${pytverstr}.a
+
+python${pytverstr}.def:
+ gendef - $(PYTHONDLL) > $@
+
+endif # win32
+
+
+SHLIB_LINK = $(python_libspec) $(python_additional_libs) $(filter -lintl,$(LIBS))
+
+REGRESS_OPTS = --dbname=$(PL_TESTDB)
+
+REGRESS = \
+ plpython_schema \
+ plpython_populate \
+ plpython_test \
+ plpython_do \
+ plpython_global \
+ plpython_import \
+ plpython_spi \
+ plpython_newline \
+ plpython_void \
+ plpython_call \
+ plpython_params \
+ plpython_setof \
+ plpython_record \
+ plpython_trigger \
+ plpython_types \
+ plpython_error \
+ plpython_ereport \
+ plpython_unicode \
+ plpython_quote \
+ plpython_composite \
+ plpython_subtransaction \
+ plpython_transaction \
+ plpython_drop
+
+include $(top_srcdir)/src/Makefile.shlib
+
+all: all-lib
+
+# Ensure parallel safety if a build is started in this directory
+$(OBJS): | submake-generated-headers
+
+install: all install-lib install-data
+
+installdirs: installdirs-lib
+ $(MKDIR_P) '$(DESTDIR)$(datadir)/extension' '$(DESTDIR)$(includedir_server)'
+
+uninstall: uninstall-lib uninstall-data
+
+install-data: installdirs
+ $(INSTALL_DATA) $(addprefix $(srcdir)/, $(DATA)) '$(DESTDIR)$(datadir)/extension/'
+ $(INSTALL_DATA) $(addprefix $(srcdir)/, $(INCS)) '$(DESTDIR)$(includedir_server)'
+
+uninstall-data:
+ rm -f $(addprefix '$(DESTDIR)$(datadir)/extension'/, $(notdir $(DATA)))
+ rm -f $(addprefix '$(DESTDIR)$(includedir_server)'/, $(INCS))
+
+.PHONY: install-data uninstall-data
+
+
+check: submake-pg-regress
+ $(pg_regress_check) $(REGRESS_OPTS) $(REGRESS)
+
+installcheck: submake-pg-regress
+ $(pg_regress_installcheck) $(REGRESS_OPTS) $(REGRESS)
+
+
+.PHONY: submake-pg-regress
+submake-pg-regress: | submake-generated-headers
+ $(MAKE) -C $(top_builddir)/src/test/regress pg_regress$(X)
+
+clean distclean: clean-lib
+ rm -f $(OBJS)
+ rm -rf $(pg_regress_clean_files)
+ifeq ($(PORTNAME), win32)
+ rm -f python${pytverstr}.def
+endif
+
+
+# Force this dependency to be known even without dependency info built:
+plpy_plpymodule.o: spiexceptions.h
+
+spiexceptions.h: $(top_srcdir)/src/backend/utils/errcodes.txt generate-spiexceptions.pl
+ $(PERL) $(srcdir)/generate-spiexceptions.pl $< > $@
+
+distprep: spiexceptions.h
+
+maintainer-clean: distclean
+ rm -f spiexceptions.h
diff --git a/src/pl/plpython/expected/README b/src/pl/plpython/expected/README
new file mode 100644
index 0000000..388c553
--- /dev/null
+++ b/src/pl/plpython/expected/README
@@ -0,0 +1,3 @@
+Guide to alternative expected files:
+
+plpython_error_5.out Python 3.5 and newer
diff --git a/src/pl/plpython/expected/plpython_call.out b/src/pl/plpython/expected/plpython_call.out
new file mode 100644
index 0000000..4c06900
--- /dev/null
+++ b/src/pl/plpython/expected/plpython_call.out
@@ -0,0 +1,75 @@
+--
+-- 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();
+ERROR: PL/Python procedure did not return None
+CONTEXT: PL/Python procedure "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;
+ a
+----
+ 55
+(1 row)
+
+-- output arguments
+CREATE PROCEDURE test_proc5(INOUT a text)
+LANGUAGE plpython3u
+AS $$
+return [a + '+' + a]
+$$;
+CALL test_proc5('abc');
+ a
+---------
+ abc+abc
+(1 row)
+
+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);
+ b | c
+---+---
+ 6 | 8
+(1 row)
+
+-- 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
+$$;
+NOTICE: a: 10
+NOTICE: _a: 10, _b: 20
+DROP PROCEDURE test_proc1;
+DROP PROCEDURE test_proc2;
+DROP PROCEDURE test_proc3;
+DROP TABLE test1;
diff --git a/src/pl/plpython/expected/plpython_composite.out b/src/pl/plpython/expected/plpython_composite.out
new file mode 100644
index 0000000..bb101e0
--- /dev/null
+++ b/src/pl/plpython/expected/plpython_composite.out
@@ -0,0 +1,594 @@
+CREATE FUNCTION multiout_simple(OUT i integer, OUT j integer) AS $$
+return (1, 2)
+$$ LANGUAGE plpython3u;
+SELECT multiout_simple();
+ multiout_simple
+-----------------
+ (1,2)
+(1 row)
+
+SELECT * FROM multiout_simple();
+ i | j
+---+---
+ 1 | 2
+(1 row)
+
+SELECT i, j + 2 FROM multiout_simple();
+ i | ?column?
+---+----------
+ 1 | 4
+(1 row)
+
+SELECT (multiout_simple()).j + 3;
+ ?column?
+----------
+ 5
+(1 row)
+
+CREATE FUNCTION multiout_simple_setof(n integer = 1, OUT integer, OUT integer) RETURNS SETOF record AS $$
+return [(1, 2)] * n
+$$ LANGUAGE plpython3u;
+SELECT multiout_simple_setof();
+ multiout_simple_setof
+-----------------------
+ (1,2)
+(1 row)
+
+SELECT * FROM multiout_simple_setof();
+ column1 | column2
+---------+---------
+ 1 | 2
+(1 row)
+
+SELECT * FROM multiout_simple_setof(3);
+ column1 | column2
+---------+---------
+ 1 | 2
+ 1 | 2
+ 1 | 2
+(3 rows)
+
+CREATE FUNCTION multiout_record_as(typ text,
+ first text, OUT first text,
+ second integer, OUT second integer,
+ retnull boolean) RETURNS record AS $$
+if retnull:
+ return None
+if typ == 'dict':
+ return { 'first': first, 'second': second, 'additionalfield': 'must not cause trouble' }
+elif typ == 'tuple':
+ return ( first, second )
+elif typ == 'list':
+ return [ first, second ]
+elif typ == 'obj':
+ class type_record: pass
+ type_record.first = first
+ type_record.second = second
+ return type_record
+elif typ == 'str':
+ return "('%s',%r)" % (first, second)
+$$ LANGUAGE plpython3u;
+SELECT * FROM multiout_record_as('dict', 'foo', 1, 'f');
+ first | second
+-------+--------
+ foo | 1
+(1 row)
+
+SELECT multiout_record_as('dict', 'foo', 1, 'f');
+ multiout_record_as
+--------------------
+ (foo,1)
+(1 row)
+
+SELECT * FROM multiout_record_as('dict', null, null, false);
+ first | second
+-------+--------
+ |
+(1 row)
+
+SELECT * FROM multiout_record_as('dict', 'one', null, false);
+ first | second
+-------+--------
+ one |
+(1 row)
+
+SELECT * FROM multiout_record_as('dict', null, 2, false);
+ first | second
+-------+--------
+ | 2
+(1 row)
+
+SELECT * FROM multiout_record_as('dict', 'three', 3, false);
+ first | second
+-------+--------
+ three | 3
+(1 row)
+
+SELECT * FROM multiout_record_as('dict', null, null, true);
+ first | second
+-------+--------
+ |
+(1 row)
+
+SELECT * FROM multiout_record_as('tuple', null, null, false);
+ first | second
+-------+--------
+ |
+(1 row)
+
+SELECT * FROM multiout_record_as('tuple', 'one', null, false);
+ first | second
+-------+--------
+ one |
+(1 row)
+
+SELECT * FROM multiout_record_as('tuple', null, 2, false);
+ first | second
+-------+--------
+ | 2
+(1 row)
+
+SELECT * FROM multiout_record_as('tuple', 'three', 3, false);
+ first | second
+-------+--------
+ three | 3
+(1 row)
+
+SELECT * FROM multiout_record_as('tuple', null, null, true);
+ first | second
+-------+--------
+ |
+(1 row)
+
+SELECT * FROM multiout_record_as('list', null, null, false);
+ first | second
+-------+--------
+ |
+(1 row)
+
+SELECT * FROM multiout_record_as('list', 'one', null, false);
+ first | second
+-------+--------
+ one |
+(1 row)
+
+SELECT * FROM multiout_record_as('list', null, 2, false);
+ first | second
+-------+--------
+ | 2
+(1 row)
+
+SELECT * FROM multiout_record_as('list', 'three', 3, false);
+ first | second
+-------+--------
+ three | 3
+(1 row)
+
+SELECT * FROM multiout_record_as('list', null, null, true);
+ first | second
+-------+--------
+ |
+(1 row)
+
+SELECT * FROM multiout_record_as('obj', null, null, false);
+ first | second
+-------+--------
+ |
+(1 row)
+
+SELECT * FROM multiout_record_as('obj', 'one', null, false);
+ first | second
+-------+--------
+ one |
+(1 row)
+
+SELECT * FROM multiout_record_as('obj', null, 2, false);
+ first | second
+-------+--------
+ | 2
+(1 row)
+
+SELECT * FROM multiout_record_as('obj', 'three', 3, false);
+ first | second
+-------+--------
+ three | 3
+(1 row)
+
+SELECT * FROM multiout_record_as('obj', null, null, true);
+ first | second
+-------+--------
+ |
+(1 row)
+
+SELECT * FROM multiout_record_as('str', 'one', 1, false);
+ first | second
+-------+--------
+ 'one' | 1
+(1 row)
+
+SELECT * FROM multiout_record_as('str', 'one', 2, false);
+ first | second
+-------+--------
+ 'one' | 2
+(1 row)
+
+SELECT *, s IS NULL AS snull FROM multiout_record_as('tuple', 'xxx', NULL, 'f') AS f(f, s);
+ f | s | snull
+-----+---+-------
+ xxx | | t
+(1 row)
+
+SELECT *, f IS NULL AS fnull, s IS NULL AS snull FROM multiout_record_as('tuple', 'xxx', 1, 't') AS f(f, s);
+ f | s | fnull | snull
+---+---+-------+-------
+ | | t | t
+(1 row)
+
+SELECT * FROM multiout_record_as('obj', NULL, 10, 'f');
+ first | second
+-------+--------
+ | 10
+(1 row)
+
+CREATE FUNCTION multiout_setof(n integer,
+ OUT power_of_2 integer,
+ OUT length integer) RETURNS SETOF record AS $$
+for i in range(n):
+ power = 2 ** i
+ length = plpy.execute("select length('%d')" % power)[0]['length']
+ yield power, length
+$$ LANGUAGE plpython3u;
+SELECT * FROM multiout_setof(3);
+ power_of_2 | length
+------------+--------
+ 1 | 1
+ 2 | 1
+ 4 | 1
+(3 rows)
+
+SELECT multiout_setof(5);
+ multiout_setof
+----------------
+ (1,1)
+ (2,1)
+ (4,1)
+ (8,1)
+ (16,2)
+(5 rows)
+
+CREATE FUNCTION multiout_return_table() RETURNS TABLE (x integer, y text) AS $$
+return [{'x': 4, 'y' :'four'},
+ {'x': 7, 'y' :'seven'},
+ {'x': 0, 'y' :'zero'}]
+$$ LANGUAGE plpython3u;
+SELECT * FROM multiout_return_table();
+ x | y
+---+-------
+ 4 | four
+ 7 | seven
+ 0 | zero
+(3 rows)
+
+CREATE FUNCTION multiout_array(OUT integer[], OUT text) RETURNS SETOF record AS $$
+yield [[1], 'a']
+yield [[1,2], 'b']
+yield [[1,2,3], None]
+$$ LANGUAGE plpython3u;
+SELECT * FROM multiout_array();
+ column1 | column2
+---------+---------
+ {1} | a
+ {1,2} | b
+ {1,2,3} |
+(3 rows)
+
+CREATE FUNCTION singleout_composite(OUT type_record) AS $$
+return {'first': 1, 'second': 2}
+$$ LANGUAGE 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();
+ first | second
+-------+--------
+ 1 | 2
+(1 row)
+
+SELECT * FROM multiout_composite();
+ first | second
+-------+--------
+ 1 | 2
+ 3 | 4
+(2 rows)
+
+-- composite OUT parameters in functions returning RECORD not supported yet
+CREATE FUNCTION multiout_composite(INOUT n integer, OUT type_record) AS $$
+return (n, (n * 2, n * 3))
+$$ LANGUAGE 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);
+ n | column2
+---+---------
+ 2 | (4,6)
+(1 row)
+
+SELECT * FROM multiout_table_type_setof('dict', 'f', 3);
+ n | column2
+---+---------
+ 0 | (6,9)
+ 1 | (6,9)
+ 2 | (6,9)
+(3 rows)
+
+SELECT * FROM multiout_table_type_setof('dict', 'f', 7);
+ n | column2
+---+---------
+ 0 | (14,21)
+ 1 | (14,21)
+ 2 | (14,21)
+ 3 | (14,21)
+ 4 | (14,21)
+ 5 | (14,21)
+ 6 | (14,21)
+(7 rows)
+
+SELECT * FROM multiout_table_type_setof('tuple', 'f', 2);
+ n | column2
+---+---------
+ 0 | (4,6)
+ 1 | (4,6)
+(2 rows)
+
+SELECT * FROM multiout_table_type_setof('tuple', 'f', 3);
+ n | column2
+---+---------
+ 0 | (6,9)
+ 1 | (6,9)
+ 2 | (6,9)
+(3 rows)
+
+SELECT * FROM multiout_table_type_setof('list', 'f', 2);
+ n | column2
+---+---------
+ 0 | (4,6)
+ 1 | (4,6)
+(2 rows)
+
+SELECT * FROM multiout_table_type_setof('list', 'f', 3);
+ n | column2
+---+---------
+ 0 | (6,9)
+ 1 | (6,9)
+ 2 | (6,9)
+(3 rows)
+
+SELECT * FROM multiout_table_type_setof('obj', 'f', 4);
+ n | column2
+---+---------
+ 0 | (8,12)
+ 1 | (8,12)
+ 2 | (8,12)
+ 3 | (8,12)
+(4 rows)
+
+SELECT * FROM multiout_table_type_setof('obj', 'f', 5);
+ n | column2
+---+---------
+ 0 | (10,15)
+ 1 | (10,15)
+ 2 | (10,15)
+ 3 | (10,15)
+ 4 | (10,15)
+(5 rows)
+
+SELECT * FROM multiout_table_type_setof('str', 'f', 6);
+ n | column2
+---+---------
+ 0 | (12,18)
+ 1 | (12,18)
+ 2 | (12,18)
+ 3 | (12,18)
+ 4 | (12,18)
+ 5 | (12,18)
+(6 rows)
+
+SELECT * FROM multiout_table_type_setof('str', 'f', 7);
+ n | column2
+---+---------
+ 0 | (14,21)
+ 1 | (14,21)
+ 2 | (14,21)
+ 3 | (14,21)
+ 4 | (14,21)
+ 5 | (14,21)
+ 6 | (14,21)
+(7 rows)
+
+SELECT * FROM multiout_table_type_setof('dict', 't', 3);
+ n | column2
+---+---------
+ 0 |
+ 1 |
+ 2 |
+(3 rows)
+
+-- check what happens if a type changes under us
+CREATE TABLE changing (
+ i integer,
+ j integer
+);
+CREATE FUNCTION changing_test(OUT n integer, OUT changing) RETURNS SETOF record AS $$
+return [(1, {'i': 1, 'j': 2}),
+ (1, (3, 4))]
+$$ LANGUAGE plpython3u;
+SELECT * FROM changing_test();
+ n | column2
+---+---------
+ 1 | (1,2)
+ 1 | (3,4)
+(2 rows)
+
+ALTER TABLE changing DROP COLUMN j;
+SELECT * FROM changing_test();
+ERROR: length of returned sequence did not match number of columns in row
+CONTEXT: while creating return value
+PL/Python function "changing_test"
+SELECT * FROM changing_test();
+ERROR: length of returned sequence did not match number of columns in row
+CONTEXT: while creating return value
+PL/Python function "changing_test"
+ALTER TABLE changing ADD COLUMN j integer;
+SELECT * FROM changing_test();
+ n | column2
+---+---------
+ 1 | (1,2)
+ 1 | (3,4)
+(2 rows)
+
+-- tables of composite types
+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();
+ tab | typ
+----------------------------+----------------------------
+ {"(first,1)","(second,2)"} | {"(third,3)","(fourth,4)"}
+ {"(first,1)","(second,2)"} | {"(third,3)","(fourth,4)"}
+ {"(first,1)","(second,2)"} | {"(third,3)","(fourth,4)"}
+(3 rows)
+
+-- 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);
+ t | val
+-----+-----
+ abc | 10
+(1 row)
+
+SELECT * FROM return_record('abc') AS r(t text, val bigint);
+ t | val
+-----+-----
+ abc | 10
+(1 row)
+
+SELECT * FROM return_record('abc') AS r(t text, val integer);
+ t | val
+-----+-----
+ abc | 10
+(1 row)
+
+SELECT * FROM return_record('abc') AS r(t varchar(30), val integer);
+ t | val
+-----+-----
+ abc | 10
+(1 row)
+
+SELECT * FROM return_record('abc') AS r(t varchar(100), val integer);
+ t | val
+-----+-----
+ abc | 10
+(1 row)
+
+SELECT * FROM return_record('999') AS r(val text, t integer);
+ val | t
+-----+-----
+ 10 | 999
+(1 row)
+
+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);
+ v3 | v2 | v1
+----+----+----
+ 3 | 2 | 1
+(1 row)
+
+SELECT * FROM return_record_2('v3') AS (v2 int, v3 int, v1 int);
+ v2 | v3 | v1
+----+----+----
+ 2 | 3 | 1
+(1 row)
+
+SELECT * FROM return_record_2('v4') AS (v1 int, v4 int, v2 int);
+ v1 | v4 | v2
+----+----+----
+ 1 | 3 | 2
+(1 row)
+
+SELECT * FROM return_record_2('v4') AS (v1 int, v4 int, v2 int);
+ v1 | v4 | v2
+----+----+----
+ 1 | 3 | 2
+(1 row)
+
+-- error
+SELECT * FROM return_record_2('v4') AS (v1 int, v3 int, v2 int);
+ERROR: key "v3" not found in mapping
+HINT: To return null in a column, add the value None to the mapping with the key named after the column.
+CONTEXT: while creating return value
+PL/Python function "return_record_2"
+-- works
+SELECT * FROM return_record_2('v3') AS (v1 int, v3 int, v2 int);
+ v1 | v3 | v2
+----+----+----
+ 1 | 3 | 2
+(1 row)
+
+SELECT * FROM return_record_2('v3') AS (v1 int, v2 int, v3 int);
+ v1 | v2 | v3
+----+----+----
+ 1 | 2 | 3
+(1 row)
+
+-- 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();
+ composite_type_as_list
+------------------------------------------------------------------------------------
+ {{"(first,1)","(second,1)"},{"(first,2)","(second,2)"},{"(first,3)","(second,3)"}}
+(1 row)
+
+-- 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();
+ERROR: malformed record literal: "first"
+DETAIL: Missing left parenthesis.
+HINT: To return a composite type in an array, return the composite type as a Python tuple, e.g., "[('foo',)]".
+CONTEXT: while creating return value
+PL/Python function "composite_type_as_list_broken"
diff --git a/src/pl/plpython/expected/plpython_do.out b/src/pl/plpython/expected/plpython_do.out
new file mode 100644
index 0000000..d131a4c
--- /dev/null
+++ b/src/pl/plpython/expected/plpython_do.out
@@ -0,0 +1,8 @@
+DO $$ plpy.notice("This is plpython3u.") $$ LANGUAGE plpython3u;
+NOTICE: This is plpython3u.
+DO $$ raise Exception("error test") $$ LANGUAGE plpython3u;
+ERROR: Exception: error test
+CONTEXT: Traceback (most recent call last):
+ PL/Python anonymous code block, line 1, in <module>
+ raise Exception("error test")
+PL/Python anonymous code block
diff --git a/src/pl/plpython/expected/plpython_drop.out b/src/pl/plpython/expected/plpython_drop.out
new file mode 100644
index 0000000..97bb54a
--- /dev/null
+++ b/src/pl/plpython/expected/plpython_drop.out
@@ -0,0 +1,5 @@
+--
+-- 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/expected/plpython_ereport.out b/src/pl/plpython/expected/plpython_ereport.out
new file mode 100644
index 0000000..0138d79
--- /dev/null
+++ b/src/pl/plpython/expected/plpython_ereport.out
@@ -0,0 +1,214 @@
+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();
+INFO: info
+DETAIL: some detail
+INFO: ()
+INFO: the question
+DETAIL: 42
+INFO: This is message text.
+DETAIL: This is detail text
+HINT: This is hint text.
+NOTICE: notice
+DETAIL: some detail
+WARNING: warning
+DETAIL: some detail
+ERROR: plpy.Error: stop on error
+DETAIL: some detail
+HINT: some hint
+CONTEXT: Traceback (most recent call last):
+ PL/Python function "elog_test", line 18, in <module>
+ plpy.error('stop on error', detail='some detail', hint='some hint')
+PL/Python function "elog_test"
+DO $$ plpy.info('other types', detail=(10, 20)) $$ LANGUAGE plpython3u;
+INFO: other types
+DETAIL: (10, 20)
+DO $$
+import time;
+from datetime import date
+plpy.info('other types', detail=date(2016, 2, 26))
+$$ LANGUAGE plpython3u;
+INFO: other types
+DETAIL: 2016-02-26
+DO $$
+basket = ['apple', 'orange', 'apple', 'pear', 'orange', 'banana']
+plpy.info('other types', detail=basket)
+$$ LANGUAGE plpython3u;
+INFO: other types
+DETAIL: ['apple', 'orange', 'apple', 'pear', 'orange', 'banana']
+-- should fail
+DO $$ plpy.info('wrong sqlstate', sqlstate='54444A') $$ LANGUAGE plpython3u;
+ERROR: ValueError: invalid SQLSTATE code
+CONTEXT: Traceback (most recent call last):
+ PL/Python anonymous code block, line 1, in <module>
+ plpy.info('wrong sqlstate', sqlstate='54444A')
+PL/Python anonymous code block
+DO $$ plpy.info('unsupported argument', blabla='fooboo') $$ LANGUAGE plpython3u;
+ERROR: TypeError: 'blabla' is an invalid keyword argument for this function
+CONTEXT: Traceback (most recent call last):
+ PL/Python anonymous code block, line 1, in <module>
+ plpy.info('unsupported argument', blabla='fooboo')
+PL/Python anonymous code block
+DO $$ plpy.info('first message', message='second message') $$ LANGUAGE plpython3u;
+ERROR: TypeError: argument 'message' given by name and position
+CONTEXT: Traceback (most recent call last):
+ PL/Python anonymous code block, line 1, in <module>
+ plpy.info('first message', message='second message')
+PL/Python anonymous code block
+DO $$ plpy.info('first message', 'second message', message='third message') $$ LANGUAGE plpython3u;
+ERROR: TypeError: argument 'message' given by name and position
+CONTEXT: Traceback (most recent call last):
+ PL/Python anonymous code block, line 1, in <module>
+ plpy.info('first message', 'second message', message='third message')
+PL/Python anonymous code block
+-- raise exception in python, handle exception in plpgsql
+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');
+ERROR: plpy.Error: hello
+DETAIL: world
+CONTEXT: Traceback (most recent call last):
+ PL/Python function "raise_exception", line 9, in <module>
+ plpy.error(**dict((k, v) for k, v in iter(kwargs.items()) if v))
+PL/Python function "raise_exception"
+SELECT raise_exception('message text', 'detail text', _sqlstate => 'YY333');
+ERROR: plpy.Error: message text
+DETAIL: detail text
+CONTEXT: Traceback (most recent call last):
+ PL/Python function "raise_exception", line 9, in <module>
+ plpy.error(**dict((k, v) for k, v in iter(kwargs.items()) if v))
+PL/Python function "raise_exception"
+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');
+ERROR: plpy.Error: message text
+DETAIL: detail text
+HINT: hint text
+CONTEXT: Traceback (most recent call last):
+ PL/Python function "raise_exception", line 9, in <module>
+ plpy.error(**dict((k, v) for k, v in iter(kwargs.items()) if v))
+PL/Python function "raise_exception"
+SELECT raise_exception(_message => 'message text',
+ _hint => 'hint text',
+ _schema_name => 'schema text',
+ _column_name => 'column text',
+ _constraint_name => 'constraint text');
+ERROR: plpy.Error: message text
+HINT: hint text
+CONTEXT: Traceback (most recent call last):
+ PL/Python function "raise_exception", line 9, in <module>
+ plpy.error(**dict((k, v) for k, v in iter(kwargs.items()) if v))
+PL/Python function "raise_exception"
+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;
+$$;
+NOTICE: handled exception
+DETAIL: message:(plpy.Error: 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)
+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;
+INFO: (119577128, None, 'some hint', None, 0, None, 'users_tab', None, 'user_type', None)
+ERROR: plpy.SPIError: plpy.Error: my message
+HINT: some hint
+CONTEXT: Traceback (most recent call last):
+ PL/Python anonymous code block, line 6, in <module>
+ raise e
+ PL/Python anonymous code block, line 3, in __plpython_inline_block
+ plpy.execute("select raise_exception(_message => 'my message', _sqlstate => 'XX987', _hint => 'some hint', _table_name => 'users_tab', _datatype_name => 'user_type')")
+PL/Python anonymous code block
+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;
+INFO: sqlstate: XX987, hint: some hint, table_name: users_tab, datatype_name: user_type
+ERROR: plpy.Error: my message
+HINT: some hint
+CONTEXT: Traceback (most recent call last):
+ PL/Python anonymous code block, line 6, in <module>
+ raise e
+ PL/Python anonymous code block, line 3, in __plpython_inline_block
+ plpy.error(message = 'my message', sqlstate = 'XX987', hint = 'some hint', table_name = 'users_tab', datatype_name = 'user_type')
+PL/Python anonymous code block
diff --git a/src/pl/plpython/expected/plpython_error.out b/src/pl/plpython/expected/plpython_error.out
new file mode 100644
index 0000000..68722b0
--- /dev/null
+++ b/src/pl/plpython/expected/plpython_error.out
@@ -0,0 +1,460 @@
+-- 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;
+ERROR: could not compile PL/Python function "python_syntax_error"
+DETAIL: SyntaxError: invalid syntax (<string>, line 2)
+/* 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();
+ERROR: could not compile PL/Python function "python_syntax_error"
+DETAIL: SyntaxError: invalid syntax (<string>, line 2)
+/* Run the function twice to check if the hashtable entry gets cleaned up */
+SELECT python_syntax_error();
+ERROR: could not compile PL/Python function "python_syntax_error"
+DETAIL: SyntaxError: invalid syntax (<string>, line 2)
+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();
+ERROR: spiexceptions.SyntaxError: syntax error at or near "syntax"
+LINE 1: syntax error
+ ^
+QUERY: syntax error
+CONTEXT: Traceback (most recent call last):
+ PL/Python function "sql_syntax_error", line 1, in <module>
+ plpy.execute("syntax error")
+PL/Python function "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');
+ERROR: IndexError: list index out of range
+CONTEXT: Traceback (most recent call last):
+ PL/Python function "exception_index_invalid", line 1, in <module>
+ return args[1]
+PL/Python function "exception_index_invalid"
+/* 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();
+ERROR: spiexceptions.UndefinedFunction: function test5(unknown) does not exist
+LINE 1: SELECT test5('foo')
+ ^
+HINT: No function matches the given name and argument types. You might need to add explicit type casts.
+QUERY: SELECT test5('foo')
+CONTEXT: Traceback (most recent call last):
+ PL/Python function "exception_index_invalid_nested", line 1, in <module>
+ rv = plpy.execute("SELECT test5('foo')")
+PL/Python function "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');
+ERROR: spiexceptions.UndefinedObject: type "test" does not exist
+CONTEXT: Traceback (most recent call last):
+ PL/Python function "invalid_type_uncaught", line 3, in <module>
+ SD["plan"] = plpy.prepare(q, [ "test" ])
+PL/Python function "invalid_type_uncaught"
+/* 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');
+NOTICE: type "test" does not exist
+ invalid_type_caught
+---------------------
+
+(1 row)
+
+/* 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');
+ERROR: plpy.Error: type "test" does not exist
+CONTEXT: Traceback (most recent call last):
+ PL/Python function "invalid_type_reraised", line 6, in <module>
+ plpy.error(str(ex))
+PL/Python function "invalid_type_reraised"
+/* 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');
+ valid_type
+------------
+
+(1 row)
+
+/* 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();
+ERROR: plpy.Error: boom
+CONTEXT: Traceback (most recent call last):
+ PL/Python function "nested_error", line 10, in <module>
+ fun3()
+ PL/Python function "nested_error", line 8, in fun3
+ fun2()
+ PL/Python function "nested_error", line 5, in fun2
+ fun1()
+ PL/Python function "nested_error", line 2, in fun1
+ plpy.error("boom")
+PL/Python function "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();
+ERROR: plpy.Error: boom
+CONTEXT: Traceback (most recent call last):
+ PL/Python function "nested_error_raise", line 10, in <module>
+ fun3()
+ PL/Python function "nested_error_raise", line 8, in fun3
+ fun2()
+ PL/Python function "nested_error_raise", line 5, in fun2
+ fun1()
+ PL/Python function "nested_error_raise", line 2, in fun1
+ raise plpy.Error("boom")
+PL/Python function "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();
+WARNING: boom
+ nested_warning
+--------------------
+ you've been warned
+(1 row)
+
+/* 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();
+ERROR: AttributeError: 'module' object has no attribute 'nonexistent'
+CONTEXT: Traceback (most recent call last):
+ PL/Python function "toplevel_attribute_error", line 2, in <module>
+ plpy.nonexistent
+PL/Python function "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();
+ERROR: spiexceptions.DivisionByZero: division by zero
+CONTEXT: Traceback (most recent call last):
+ PL/Python function "python_traceback", line 11, in <module>
+ first()
+ PL/Python function "python_traceback", line 3, in first
+ second()
+ PL/Python function "python_traceback", line 6, in second
+ third()
+ PL/Python function "python_traceback", line 9, in third
+ plpy.execute("select sql_error()")
+PL/Python function "python_traceback"
+SELECT sql_error();
+ERROR: division by zero
+CONTEXT: SQL statement "select 1/0"
+PL/pgSQL function sql_error() line 3 at SQL statement
+SELECT python_from_sql_error();
+ERROR: spiexceptions.DivisionByZero: division by zero
+CONTEXT: Traceback (most recent call last):
+ PL/Python function "python_traceback", line 11, in <module>
+ first()
+ PL/Python function "python_traceback", line 3, in first
+ second()
+ PL/Python function "python_traceback", line 6, in second
+ third()
+ PL/Python function "python_traceback", line 9, in third
+ plpy.execute("select sql_error()")
+PL/Python function "python_traceback"
+SQL statement "select python_traceback()"
+PL/pgSQL function python_from_sql_error() line 3 at SQL statement
+SELECT sql_from_python_error();
+ERROR: spiexceptions.DivisionByZero: division by zero
+CONTEXT: Traceback (most recent call last):
+ PL/Python function "sql_from_python_error", line 2, in <module>
+ plpy.execute("select sql_error()")
+PL/Python function "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);
+ specific_exception
+--------------------
+
+(1 row)
+
+SELECT specific_exception(NULL);
+NOTICE: Violated the NOT NULL constraint, sqlstate 23502
+ specific_exception
+--------------------
+
+(1 row)
+
+SELECT specific_exception(2);
+NOTICE: Violated the UNIQUE constraint, sqlstate 23505
+ specific_exception
+--------------------
+
+(1 row)
+
+/* 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();
+ catch_python_unique_violation
+-------------------------------
+ ok
+(1 row)
+
+/* 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();
+ERROR: plpy.SPIError: SPI_execute failed: SPI_ERROR_TRANSACTION
+CONTEXT: Traceback (most recent call last):
+ PL/Python function "manual_subxact", line 2, in <module>
+ plpy.execute("savepoint save")
+PL/Python function "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();
+ERROR: plpy.SPIError: SPI_execute_plan failed: SPI_ERROR_TRANSACTION
+CONTEXT: Traceback (most recent call last):
+ PL/Python function "manual_subxact_prepared", line 4, in <module>
+ plpy.execute(save)
+PL/Python function "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();
+NOTICE: inside DO
+CONTEXT: PL/Python anonymous code block
+SQL statement "DO LANGUAGE plpython3u $x$ plpy.notice('inside DO') $x$"
+PL/Python function "notice_innerfunc"
+SQL statement "SELECT notice_innerfunc()"
+PL/Python function "notice_outerfunc"
+ notice_outerfunc
+------------------
+ 1
+(1 row)
+
+/* test error logged with an underlying exception that includes a detail
+ * string (bug #18070).
+ */
+CREATE FUNCTION python_error_detail() RETURNS SETOF text AS $$
+ plan = plpy.prepare("SELECT to_date('xy', 'DD') d")
+ for row in plpy.cursor(plan):
+ yield row['d']
+$$ LANGUAGE plpython3u;
+SELECT python_error_detail();
+ERROR: error fetching next item from iterator
+DETAIL: spiexceptions.InvalidDatetimeFormat: invalid value "xy" for "DD"
+CONTEXT: Traceback (most recent call last):
+PL/Python function "python_error_detail"
diff --git a/src/pl/plpython/expected/plpython_error_5.out b/src/pl/plpython/expected/plpython_error_5.out
new file mode 100644
index 0000000..fd9cd73
--- /dev/null
+++ b/src/pl/plpython/expected/plpython_error_5.out
@@ -0,0 +1,460 @@
+-- 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;
+ERROR: could not compile PL/Python function "python_syntax_error"
+DETAIL: SyntaxError: invalid syntax (<string>, line 2)
+/* 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();
+ERROR: could not compile PL/Python function "python_syntax_error"
+DETAIL: SyntaxError: invalid syntax (<string>, line 2)
+/* Run the function twice to check if the hashtable entry gets cleaned up */
+SELECT python_syntax_error();
+ERROR: could not compile PL/Python function "python_syntax_error"
+DETAIL: SyntaxError: invalid syntax (<string>, line 2)
+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();
+ERROR: spiexceptions.SyntaxError: syntax error at or near "syntax"
+LINE 1: syntax error
+ ^
+QUERY: syntax error
+CONTEXT: Traceback (most recent call last):
+ PL/Python function "sql_syntax_error", line 1, in <module>
+ plpy.execute("syntax error")
+PL/Python function "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');
+ERROR: IndexError: list index out of range
+CONTEXT: Traceback (most recent call last):
+ PL/Python function "exception_index_invalid", line 1, in <module>
+ return args[1]
+PL/Python function "exception_index_invalid"
+/* 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();
+ERROR: spiexceptions.UndefinedFunction: function test5(unknown) does not exist
+LINE 1: SELECT test5('foo')
+ ^
+HINT: No function matches the given name and argument types. You might need to add explicit type casts.
+QUERY: SELECT test5('foo')
+CONTEXT: Traceback (most recent call last):
+ PL/Python function "exception_index_invalid_nested", line 1, in <module>
+ rv = plpy.execute("SELECT test5('foo')")
+PL/Python function "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');
+ERROR: spiexceptions.UndefinedObject: type "test" does not exist
+CONTEXT: Traceback (most recent call last):
+ PL/Python function "invalid_type_uncaught", line 3, in <module>
+ SD["plan"] = plpy.prepare(q, [ "test" ])
+PL/Python function "invalid_type_uncaught"
+/* 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');
+NOTICE: type "test" does not exist
+ invalid_type_caught
+---------------------
+
+(1 row)
+
+/* 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');
+ERROR: plpy.Error: type "test" does not exist
+CONTEXT: Traceback (most recent call last):
+ PL/Python function "invalid_type_reraised", line 6, in <module>
+ plpy.error(str(ex))
+PL/Python function "invalid_type_reraised"
+/* 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');
+ valid_type
+------------
+
+(1 row)
+
+/* 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();
+ERROR: plpy.Error: boom
+CONTEXT: Traceback (most recent call last):
+ PL/Python function "nested_error", line 10, in <module>
+ fun3()
+ PL/Python function "nested_error", line 8, in fun3
+ fun2()
+ PL/Python function "nested_error", line 5, in fun2
+ fun1()
+ PL/Python function "nested_error", line 2, in fun1
+ plpy.error("boom")
+PL/Python function "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();
+ERROR: plpy.Error: boom
+CONTEXT: Traceback (most recent call last):
+ PL/Python function "nested_error_raise", line 10, in <module>
+ fun3()
+ PL/Python function "nested_error_raise", line 8, in fun3
+ fun2()
+ PL/Python function "nested_error_raise", line 5, in fun2
+ fun1()
+ PL/Python function "nested_error_raise", line 2, in fun1
+ raise plpy.Error("boom")
+PL/Python function "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();
+WARNING: boom
+ nested_warning
+--------------------
+ you've been warned
+(1 row)
+
+/* 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();
+ERROR: AttributeError: module 'plpy' has no attribute 'nonexistent'
+CONTEXT: Traceback (most recent call last):
+ PL/Python function "toplevel_attribute_error", line 2, in <module>
+ plpy.nonexistent
+PL/Python function "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();
+ERROR: spiexceptions.DivisionByZero: division by zero
+CONTEXT: Traceback (most recent call last):
+ PL/Python function "python_traceback", line 11, in <module>
+ first()
+ PL/Python function "python_traceback", line 3, in first
+ second()
+ PL/Python function "python_traceback", line 6, in second
+ third()
+ PL/Python function "python_traceback", line 9, in third
+ plpy.execute("select sql_error()")
+PL/Python function "python_traceback"
+SELECT sql_error();
+ERROR: division by zero
+CONTEXT: SQL statement "select 1/0"
+PL/pgSQL function sql_error() line 3 at SQL statement
+SELECT python_from_sql_error();
+ERROR: spiexceptions.DivisionByZero: division by zero
+CONTEXT: Traceback (most recent call last):
+ PL/Python function "python_traceback", line 11, in <module>
+ first()
+ PL/Python function "python_traceback", line 3, in first
+ second()
+ PL/Python function "python_traceback", line 6, in second
+ third()
+ PL/Python function "python_traceback", line 9, in third
+ plpy.execute("select sql_error()")
+PL/Python function "python_traceback"
+SQL statement "select python_traceback()"
+PL/pgSQL function python_from_sql_error() line 3 at SQL statement
+SELECT sql_from_python_error();
+ERROR: spiexceptions.DivisionByZero: division by zero
+CONTEXT: Traceback (most recent call last):
+ PL/Python function "sql_from_python_error", line 2, in <module>
+ plpy.execute("select sql_error()")
+PL/Python function "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);
+ specific_exception
+--------------------
+
+(1 row)
+
+SELECT specific_exception(NULL);
+NOTICE: Violated the NOT NULL constraint, sqlstate 23502
+ specific_exception
+--------------------
+
+(1 row)
+
+SELECT specific_exception(2);
+NOTICE: Violated the UNIQUE constraint, sqlstate 23505
+ specific_exception
+--------------------
+
+(1 row)
+
+/* 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();
+ catch_python_unique_violation
+-------------------------------
+ ok
+(1 row)
+
+/* 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();
+ERROR: plpy.SPIError: SPI_execute failed: SPI_ERROR_TRANSACTION
+CONTEXT: Traceback (most recent call last):
+ PL/Python function "manual_subxact", line 2, in <module>
+ plpy.execute("savepoint save")
+PL/Python function "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();
+ERROR: plpy.SPIError: SPI_execute_plan failed: SPI_ERROR_TRANSACTION
+CONTEXT: Traceback (most recent call last):
+ PL/Python function "manual_subxact_prepared", line 4, in <module>
+ plpy.execute(save)
+PL/Python function "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();
+NOTICE: inside DO
+CONTEXT: PL/Python anonymous code block
+SQL statement "DO LANGUAGE plpython3u $x$ plpy.notice('inside DO') $x$"
+PL/Python function "notice_innerfunc"
+SQL statement "SELECT notice_innerfunc()"
+PL/Python function "notice_outerfunc"
+ notice_outerfunc
+------------------
+ 1
+(1 row)
+
+/* test error logged with an underlying exception that includes a detail
+ * string (bug #18070).
+ */
+CREATE FUNCTION python_error_detail() RETURNS SETOF text AS $$
+ plan = plpy.prepare("SELECT to_date('xy', 'DD') d")
+ for row in plpy.cursor(plan):
+ yield row['d']
+$$ LANGUAGE plpython3u;
+SELECT python_error_detail();
+ERROR: error fetching next item from iterator
+DETAIL: spiexceptions.InvalidDatetimeFormat: invalid value "xy" for "DD"
+CONTEXT: Traceback (most recent call last):
+PL/Python function "python_error_detail"
diff --git a/src/pl/plpython/expected/plpython_global.out b/src/pl/plpython/expected/plpython_global.out
new file mode 100644
index 0000000..a4cfb14
--- /dev/null
+++ b/src/pl/plpython/expected/plpython_global.out
@@ -0,0 +1,52 @@
+--
+-- 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();
+ static_test
+-------------
+ 1
+(1 row)
+
+SELECT static_test();
+ static_test
+-------------
+ 2
+(1 row)
+
+SELECT global_test_one();
+ global_test_one
+--------------------------------------------------------
+ SD: set by global_test_one, GD: set by global_test_one
+(1 row)
+
+SELECT global_test_two();
+ global_test_two
+--------------------------------------------------------
+ SD: set by global_test_two, GD: set by global_test_one
+(1 row)
+
diff --git a/src/pl/plpython/expected/plpython_import.out b/src/pl/plpython/expected/plpython_import.out
new file mode 100644
index 0000000..854e989
--- /dev/null
+++ b/src/pl/plpython/expected/plpython_import.out
@@ -0,0 +1,79 @@
+-- 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();
+ import_fail
+--------------------
+ failed as expected
+(1 row)
+
+SELECT import_succeed();
+ import_succeed
+------------------------
+ succeeded, as expected
+(1 row)
+
+-- test import and simple argument handling
+--
+SELECT import_test_one('sha hash of this string');
+ import_test_one
+------------------------------------------
+ a04e23cb9b1a09cd1051a04a7c571aae0f90346c
+(1 row)
+
+-- test import and tuple argument handling
+--
+select import_test_two(users) from users where fname = 'willem';
+ import_test_two
+-------------------------------------------------------------------
+ sha hash of willemdoe is 3cde6b574953b0ca937b4d76ebc40d534d910759
+(1 row)
+
diff --git a/src/pl/plpython/expected/plpython_newline.out b/src/pl/plpython/expected/plpython_newline.out
new file mode 100644
index 0000000..2bc1492
--- /dev/null
+++ b/src/pl/plpython/expected/plpython_newline.out
@@ -0,0 +1,30 @@
+--
+-- 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();
+ newline_lf
+------------
+ 123
+(1 row)
+
+SELECT newline_cr();
+ newline_cr
+------------
+ 123
+(1 row)
+
+SELECT newline_crlf();
+ newline_crlf
+--------------
+ 123
+(1 row)
+
diff --git a/src/pl/plpython/expected/plpython_params.out b/src/pl/plpython/expected/plpython_params.out
new file mode 100644
index 0000000..d1a36f3
--- /dev/null
+++ b/src/pl/plpython/expected/plpython_params.out
@@ -0,0 +1,64 @@
+--
+-- 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);
+ test_param_names0
+-------------------
+ 9
+(1 row)
+
+SELECT test_param_names1(1,'text');
+ test_param_names1
+-------------------
+ t
+(1 row)
+
+SELECT test_param_names2(users) from users;
+ test_param_names2
+-----------------------------------------------------------------------
+ {'fname': 'jane', 'lname': 'doe', 'userid': 1, 'username': 'j_doe'}
+ {'fname': 'john', 'lname': 'doe', 'userid': 2, 'username': 'johnd'}
+ {'fname': 'willem', 'lname': 'doe', 'userid': 3, 'username': 'w_doe'}
+ {'fname': 'rick', 'lname': 'smith', 'userid': 4, 'username': 'slash'}
+(4 rows)
+
+SELECT test_param_names2(NULL);
+ test_param_names2
+-------------------
+ None
+(1 row)
+
+SELECT test_param_names3(1);
+ test_param_names3
+-------------------
+ t
+(1 row)
+
diff --git a/src/pl/plpython/expected/plpython_populate.out b/src/pl/plpython/expected/plpython_populate.out
new file mode 100644
index 0000000..4db75b0
--- /dev/null
+++ b/src/pl/plpython/expected/plpython_populate.out
@@ -0,0 +1,22 @@
+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/expected/plpython_quote.out b/src/pl/plpython/expected/plpython_quote.out
new file mode 100644
index 0000000..1fbe93d
--- /dev/null
+++ b/src/pl/plpython/expected/plpython_quote.out
@@ -0,0 +1,56 @@
+-- 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);
+ quote
+-----------
+ 'abc'
+ 'a''bc'
+ '''abc'''
+ ''
+ ''''
+ 'xyzv'
+(6 rows)
+
+SELECT quote(t, 'nullable') FROM (VALUES
+ ('abc'),
+ ('a''bc'),
+ ('''abc'''),
+ (''),
+ (''''),
+ (NULL)) AS v(t);
+ quote
+-----------
+ 'abc'
+ 'a''bc'
+ '''abc'''
+ ''
+ ''''
+ NULL
+(6 rows)
+
+SELECT quote(t, 'ident') FROM (VALUES
+ ('abc'),
+ ('a b c'),
+ ('a " ''abc''')) AS v(t);
+ quote
+--------------
+ abc
+ "a b c"
+ "a "" 'abc'"
+(3 rows)
+
diff --git a/src/pl/plpython/expected/plpython_record.out b/src/pl/plpython/expected/plpython_record.out
new file mode 100644
index 0000000..31de198
--- /dev/null
+++ b/src/pl/plpython/expected/plpython_record.out
@@ -0,0 +1,373 @@
+--
+-- 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);
+ first | second
+-------+--------
+ |
+(1 row)
+
+SELECT * FROM test_table_record_as('dict', 'one', null, false);
+ first | second
+-------+--------
+ one |
+(1 row)
+
+SELECT * FROM test_table_record_as('dict', null, 2, false);
+ first | second
+-------+--------
+ | 2
+(1 row)
+
+SELECT * FROM test_table_record_as('dict', 'three', 3, false);
+ first | second
+-------+--------
+ three | 3
+(1 row)
+
+SELECT * FROM test_table_record_as('dict', null, null, true);
+ first | second
+-------+--------
+ |
+(1 row)
+
+SELECT * FROM test_table_record_as('tuple', null, null, false);
+ first | second
+-------+--------
+ |
+(1 row)
+
+SELECT * FROM test_table_record_as('tuple', 'one', null, false);
+ first | second
+-------+--------
+ one |
+(1 row)
+
+SELECT * FROM test_table_record_as('tuple', null, 2, false);
+ first | second
+-------+--------
+ | 2
+(1 row)
+
+SELECT * FROM test_table_record_as('tuple', 'three', 3, false);
+ first | second
+-------+--------
+ three | 3
+(1 row)
+
+SELECT * FROM test_table_record_as('tuple', null, null, true);
+ first | second
+-------+--------
+ |
+(1 row)
+
+SELECT * FROM test_table_record_as('list', null, null, false);
+ first | second
+-------+--------
+ |
+(1 row)
+
+SELECT * FROM test_table_record_as('list', 'one', null, false);
+ first | second
+-------+--------
+ one |
+(1 row)
+
+SELECT * FROM test_table_record_as('list', null, 2, false);
+ first | second
+-------+--------
+ | 2
+(1 row)
+
+SELECT * FROM test_table_record_as('list', 'three', 3, false);
+ first | second
+-------+--------
+ three | 3
+(1 row)
+
+SELECT * FROM test_table_record_as('list', null, null, true);
+ first | second
+-------+--------
+ |
+(1 row)
+
+SELECT * FROM test_table_record_as('obj', null, null, false);
+ first | second
+-------+--------
+ |
+(1 row)
+
+SELECT * FROM test_table_record_as('obj', 'one', null, false);
+ first | second
+-------+--------
+ one |
+(1 row)
+
+SELECT * FROM test_table_record_as('obj', null, 2, false);
+ first | second
+-------+--------
+ | 2
+(1 row)
+
+SELECT * FROM test_table_record_as('obj', 'three', 3, false);
+ first | second
+-------+--------
+ three | 3
+(1 row)
+
+SELECT * FROM test_table_record_as('obj', null, null, true);
+ first | second
+-------+--------
+ |
+(1 row)
+
+SELECT * FROM test_type_record_as('dict', null, null, false);
+ first | second
+-------+--------
+ |
+(1 row)
+
+SELECT * FROM test_type_record_as('dict', 'one', null, false);
+ first | second
+-------+--------
+ one |
+(1 row)
+
+SELECT * FROM test_type_record_as('dict', null, 2, false);
+ first | second
+-------+--------
+ | 2
+(1 row)
+
+SELECT * FROM test_type_record_as('dict', 'three', 3, false);
+ first | second
+-------+--------
+ three | 3
+(1 row)
+
+SELECT * FROM test_type_record_as('dict', null, null, true);
+ first | second
+-------+--------
+ |
+(1 row)
+
+SELECT * FROM test_type_record_as('tuple', null, null, false);
+ first | second
+-------+--------
+ |
+(1 row)
+
+SELECT * FROM test_type_record_as('tuple', 'one', null, false);
+ first | second
+-------+--------
+ one |
+(1 row)
+
+SELECT * FROM test_type_record_as('tuple', null, 2, false);
+ first | second
+-------+--------
+ | 2
+(1 row)
+
+SELECT * FROM test_type_record_as('tuple', 'three', 3, false);
+ first | second
+-------+--------
+ three | 3
+(1 row)
+
+SELECT * FROM test_type_record_as('tuple', null, null, true);
+ first | second
+-------+--------
+ |
+(1 row)
+
+SELECT * FROM test_type_record_as('list', null, null, false);
+ first | second
+-------+--------
+ |
+(1 row)
+
+SELECT * FROM test_type_record_as('list', 'one', null, false);
+ first | second
+-------+--------
+ one |
+(1 row)
+
+SELECT * FROM test_type_record_as('list', null, 2, false);
+ first | second
+-------+--------
+ | 2
+(1 row)
+
+SELECT * FROM test_type_record_as('list', 'three', 3, false);
+ first | second
+-------+--------
+ three | 3
+(1 row)
+
+SELECT * FROM test_type_record_as('list', null, null, true);
+ first | second
+-------+--------
+ |
+(1 row)
+
+SELECT * FROM test_type_record_as('obj', null, null, false);
+ first | second
+-------+--------
+ |
+(1 row)
+
+SELECT * FROM test_type_record_as('obj', 'one', null, false);
+ first | second
+-------+--------
+ one |
+(1 row)
+
+SELECT * FROM test_type_record_as('obj', null, 2, false);
+ first | second
+-------+--------
+ | 2
+(1 row)
+
+SELECT * FROM test_type_record_as('obj', 'three', 3, false);
+ first | second
+-------+--------
+ three | 3
+(1 row)
+
+SELECT * FROM test_type_record_as('obj', null, null, true);
+ first | second
+-------+--------
+ |
+(1 row)
+
+SELECT * FROM test_type_record_as('str', 'one', 1, false);
+ first | second
+-------+--------
+ 'one' | 1
+(1 row)
+
+SELECT * FROM test_in_out_params('test_in');
+ second
+-------------------
+ test_in_in_to_out
+(1 row)
+
+SELECT * FROM test_in_out_params_multi('test_in');
+ second | third
+----------------------------+----------------------------
+ test_in_record_in_to_out_1 | test_in_record_in_to_out_2
+(1 row)
+
+SELECT * FROM test_inout_params('test_in');
+ first
+---------------
+ test_in_inout
+(1 row)
+
+-- 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);
+ first | second
+-------+--------
+ one | 1
+(1 row)
+
+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);
+ first | second
+-------+--------
+ one | 1
+(1 row)
+
+-- errors cases
+CREATE FUNCTION test_type_record_error1() RETURNS type_record AS $$
+ return { 'first': 'first' }
+$$ LANGUAGE plpython3u;
+SELECT * FROM test_type_record_error1();
+ERROR: key "second" not found in mapping
+HINT: To return null in a column, add the value None to the mapping with the key named after the column.
+CONTEXT: while creating return value
+PL/Python function "test_type_record_error1"
+CREATE FUNCTION test_type_record_error2() RETURNS type_record AS $$
+ return [ 'first' ]
+$$ LANGUAGE plpython3u;
+SELECT * FROM test_type_record_error2();
+ERROR: length of returned sequence did not match number of columns in row
+CONTEXT: while creating return value
+PL/Python function "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();
+ERROR: attribute "second" does not exist in Python object
+HINT: To return null in a column, let the returned object have an attribute named after column with value None.
+CONTEXT: while creating return value
+PL/Python function "test_type_record_error3"
+CREATE FUNCTION test_type_record_error4() RETURNS type_record AS $$
+ return 'foo'
+$$ LANGUAGE plpython3u;
+SELECT * FROM test_type_record_error4();
+ERROR: malformed record literal: "foo"
+DETAIL: Missing left parenthesis.
+CONTEXT: while creating return value
+PL/Python function "test_type_record_error4"
diff --git a/src/pl/plpython/expected/plpython_schema.out b/src/pl/plpython/expected/plpython_schema.out
new file mode 100644
index 0000000..23d94f6
--- /dev/null
+++ b/src/pl/plpython/expected/plpython_schema.out
@@ -0,0 +1,33 @@
+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/expected/plpython_setof.out b/src/pl/plpython/expected/plpython_setof.out
new file mode 100644
index 0000000..3940940
--- /dev/null
+++ b/src/pl/plpython/expected/plpython_setof.out
@@ -0,0 +1,200 @@
+--
+-- Test returning SETOF
+--
+CREATE FUNCTION test_setof_error() RETURNS SETOF text AS $$
+return 37
+$$ LANGUAGE plpython3u;
+SELECT test_setof_error();
+ERROR: returned object cannot be iterated
+DETAIL: PL/Python set-returning functions must return an iterable object.
+CONTEXT: PL/Python function "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');
+ test_setof_as_list
+--------------------
+(0 rows)
+
+SELECT test_setof_as_list(1, 'list');
+ test_setof_as_list
+--------------------
+ list
+(1 row)
+
+SELECT test_setof_as_list(2, 'list');
+ test_setof_as_list
+--------------------
+ list
+ list
+(2 rows)
+
+SELECT test_setof_as_list(2, null);
+ test_setof_as_list
+--------------------
+
+
+(2 rows)
+
+SELECT test_setof_as_tuple(0, 'tuple');
+ test_setof_as_tuple
+---------------------
+(0 rows)
+
+SELECT test_setof_as_tuple(1, 'tuple');
+ test_setof_as_tuple
+---------------------
+ tuple
+(1 row)
+
+SELECT test_setof_as_tuple(2, 'tuple');
+ test_setof_as_tuple
+---------------------
+ tuple
+ tuple
+(2 rows)
+
+SELECT test_setof_as_tuple(2, null);
+ test_setof_as_tuple
+---------------------
+
+
+(2 rows)
+
+SELECT test_setof_as_iterator(0, 'list');
+ test_setof_as_iterator
+------------------------
+(0 rows)
+
+SELECT test_setof_as_iterator(1, 'list');
+ test_setof_as_iterator
+------------------------
+ list
+(1 row)
+
+SELECT test_setof_as_iterator(2, 'list');
+ test_setof_as_iterator
+------------------------
+ list
+ list
+(2 rows)
+
+SELECT test_setof_as_iterator(2, null);
+ test_setof_as_iterator
+------------------------
+
+
+(2 rows)
+
+SELECT test_setof_spi_in_iterator();
+ test_setof_spi_in_iterator
+----------------------------
+ Hello
+ Brave
+ New
+ World
+(4 rows)
+
+-- 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);
+ ugly
+------
+ 1
+ 2
+ 3
+ 4
+ 5
+(5 rows)
+
+-- interleaved execution of such a function
+SELECT ugly(1,3), ugly(7,8);
+ ugly | ugly
+------+------
+ 1 | 7
+ 2 | 8
+ 3 |
+(3 rows)
+
+-- 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();
+ get_user_records
+----------------------
+ (jane,doe,j_doe,1)
+ (john,doe,johnd,2)
+ (rick,smith,slash,4)
+ (willem,doe,w_doe,3)
+(4 rows)
+
+SELECT * FROM get_user_records();
+ fname | lname | username | userid
+--------+-------+----------+--------
+ jane | doe | j_doe | 1
+ john | doe | johnd | 2
+ rick | smith | slash | 4
+ willem | doe | w_doe | 3
+(4 rows)
+
+-- 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();
+ get_user_records2
+----------------------
+ (jane,doe,j_doe,1)
+ (john,doe,johnd,2)
+ (rick,smith,slash,4)
+ (willem,doe,w_doe,3)
+(4 rows)
+
+SELECT * FROM get_user_records2();
+ fname | lname | username | userid
+--------+-------+----------+--------
+ jane | doe | j_doe | 1
+ john | doe | johnd | 2
+ rick | smith | slash | 4
+ willem | doe | w_doe | 3
+(4 rows)
+
diff --git a/src/pl/plpython/expected/plpython_spi.out b/src/pl/plpython/expected/plpython_spi.out
new file mode 100644
index 0000000..8853e25
--- /dev/null
+++ b/src/pl/plpython/expected/plpython_spi.out
@@ -0,0 +1,466 @@
+--
+-- 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');
+ nested_call_one
+-----------------------------------------------------------------
+ {'nested_call_two': "{'nested_call_three': 'pass this along'}"}
+(1 row)
+
+select spi_prepared_plan_test_one('doe');
+ spi_prepared_plan_test_one
+----------------------------
+ there are 3 does
+(1 row)
+
+select spi_prepared_plan_test_two('smith');
+ spi_prepared_plan_test_two
+----------------------------
+ there are 1 smiths
+(1 row)
+
+select spi_prepared_plan_test_nested('smith');
+ spi_prepared_plan_test_nested
+-------------------------------
+ there are 1 smiths
+(1 row)
+
+SELECT join_sequences(sequences) FROM sequences;
+ join_sequences
+----------------
+ ABCDEFGHIJKL
+ ABCDEF
+ ABCDEF
+ ABCDEF
+ ABCDEF
+ ABCDEF
+(6 rows)
+
+SELECT join_sequences(sequences) FROM sequences
+ WHERE join_sequences(sequences) ~* '^A';
+ join_sequences
+----------------
+ ABCDEFGHIJKL
+ ABCDEF
+ ABCDEF
+ ABCDEF
+ ABCDEF
+ ABCDEF
+(6 rows)
+
+SELECT join_sequences(sequences) FROM sequences
+ WHERE join_sequences(sequences) ~* '^B';
+ join_sequences
+----------------
+(0 rows)
+
+SELECT spi_recursive_sum(10);
+ spi_recursive_sum
+-------------------
+ 55
+(1 row)
+
+--
+-- 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'$$);
+INFO: True
+INFO: ['foo', 'bar']
+INFO: [23, 25]
+INFO: [-1, -1]
+ result_metadata_test
+----------------------
+ 2
+(1 row)
+
+SELECT result_metadata_test($$CREATE TEMPORARY TABLE foo1 (a int, b text)$$);
+INFO: True
+ERROR: plpy.Error: command did not produce a result set
+CONTEXT: Traceback (most recent call last):
+ PL/Python function "result_metadata_test", line 6, in <module>
+ plpy.info(result.colnames())
+PL/Python function "result_metadata_test"
+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$$);
+ result_nrows_test
+-------------------
+ 1
+(1 row)
+
+SELECT result_nrows_test($$CREATE TEMPORARY TABLE foo2 (a int, b text)$$);
+ result_nrows_test
+-------------------
+ 0
+(1 row)
+
+SELECT result_nrows_test($$INSERT INTO foo2 VALUES (1, 'one'), (2, 'two')$$);
+ result_nrows_test
+-------------------
+ 2
+(1 row)
+
+SELECT result_nrows_test($$UPDATE foo2 SET b = '' WHERE a = 2$$);
+ result_nrows_test
+-------------------
+ 1
+(1 row)
+
+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$$);
+ result_len_test
+-----------------
+ 1
+(1 row)
+
+SELECT result_len_test($$CREATE TEMPORARY TABLE foo3 (a int, b text)$$);
+ result_len_test
+-----------------
+ 0
+(1 row)
+
+SELECT result_len_test($$INSERT INTO foo3 VALUES (1, 'one'), (2, 'two')$$);
+ result_len_test
+-----------------
+ 0
+(1 row)
+
+SELECT result_len_test($$UPDATE foo3 SET b= '' WHERE a = 2$$);
+ result_len_test
+-----------------
+ 0
+(1 row)
+
+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();
+INFO: 2
+INFO: 4
+INFO: [2, 3]
+INFO: [1, 3]
+INFO: [10, 100, 3, 1000]
+ result_subscript_test
+-----------------------
+
+(1 row)
+
+CREATE FUNCTION result_empty_test() RETURNS void
+AS $$
+result = plpy.execute("select 1 where false")
+
+plpy.info(result[:])
+
+$$ LANGUAGE plpython3u;
+SELECT result_empty_test();
+INFO: []
+ result_empty_test
+-------------------
+
+(1 row)
+
+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$$);
+ result_str_test
+------------------------------------------------------------
+ <PLyResult status=5 nrows=2 rows=[{'foo': 1}, {'foo': 2}]>
+(1 row)
+
+SELECT result_str_test($$CREATE TEMPORARY TABLE foo1 (a int, b text)$$);
+ result_str_test
+--------------------------------------
+ <PLyResult status=4 nrows=0 rows=[]>
+(1 row)
+
+-- 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();
+ simple_cursor_test
+--------------------
+ 3
+(1 row)
+
+SELECT double_cursor_close();
+ double_cursor_close
+---------------------
+
+(1 row)
+
+SELECT cursor_fetch();
+ cursor_fetch
+--------------
+
+(1 row)
+
+SELECT cursor_mix_next_and_fetch();
+ cursor_mix_next_and_fetch
+---------------------------
+
+(1 row)
+
+SELECT fetch_after_close();
+ fetch_after_close
+-------------------
+
+(1 row)
+
+SELECT next_after_close();
+ next_after_close
+------------------
+
+(1 row)
+
+SELECT cursor_fetch_next_empty();
+ cursor_fetch_next_empty
+-------------------------
+
+(1 row)
+
+SELECT cursor_plan();
+ cursor_plan
+-------------
+ willem
+ jane
+ john
+(3 rows)
+
+SELECT cursor_plan_wrong_args();
+ERROR: TypeError: Expected sequence of 1 argument, got 2: ['a', 'b']
+CONTEXT: Traceback (most recent call last):
+ PL/Python function "cursor_plan_wrong_args", line 4, in <module>
+ c = plpy.cursor(plan, ["a", "b"])
+PL/Python function "cursor_plan_wrong_args"
+SELECT plan_composite_args();
+ plan_composite_args
+---------------------
+ (3,label)
+(1 row)
+
diff --git a/src/pl/plpython/expected/plpython_subtransaction.out b/src/pl/plpython/expected/plpython_subtransaction.out
new file mode 100644
index 0000000..43d9277
--- /dev/null
+++ b/src/pl/plpython/expected/plpython_subtransaction.out
@@ -0,0 +1,401 @@
+--
+-- 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();
+ subtransaction_ctx_test
+-------------------------
+
+(1 row)
+
+SELECT * FROM subtransaction_tbl;
+ i
+---
+ 1
+ 2
+(2 rows)
+
+TRUNCATE subtransaction_tbl;
+SELECT subtransaction_ctx_test('SPI');
+ERROR: spiexceptions.InvalidTextRepresentation: invalid input syntax for type integer: "oops"
+LINE 1: INSERT INTO subtransaction_tbl VALUES ('oops')
+ ^
+QUERY: INSERT INTO subtransaction_tbl VALUES ('oops')
+CONTEXT: Traceback (most recent call last):
+ PL/Python function "subtransaction_ctx_test", line 6, in <module>
+ plpy.execute("INSERT INTO subtransaction_tbl VALUES ('oops')")
+PL/Python function "subtransaction_ctx_test"
+SELECT * FROM subtransaction_tbl;
+ i
+---
+(0 rows)
+
+TRUNCATE subtransaction_tbl;
+SELECT subtransaction_ctx_test('Python');
+ERROR: Exception: Python exception
+CONTEXT: Traceback (most recent call last):
+ PL/Python function "subtransaction_ctx_test", line 8, in <module>
+ raise Exception("Python exception")
+PL/Python function "subtransaction_ctx_test"
+SELECT * FROM subtransaction_tbl;
+ i
+---
+(0 rows)
+
+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();
+ERROR: spiexceptions.SyntaxError: syntax error at or near "error"
+LINE 1: error
+ ^
+QUERY: error
+CONTEXT: Traceback (most recent call last):
+ PL/Python function "subtransaction_nested_test", line 8, in <module>
+ plpy.execute("error")
+PL/Python function "subtransaction_nested_test"
+SELECT * FROM subtransaction_tbl;
+ i
+---
+(0 rows)
+
+TRUNCATE subtransaction_tbl;
+SELECT subtransaction_nested_test('t');
+NOTICE: Swallowed SyntaxError('syntax error at or near "error"')
+ subtransaction_nested_test
+----------------------------
+ ok
+(1 row)
+
+SELECT * FROM subtransaction_tbl;
+ i
+---
+ 1
+ 2
+(2 rows)
+
+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();
+NOTICE: Swallowed SyntaxError('syntax error at or near "error"')
+ subtransaction_deeply_nested_test
+-----------------------------------
+ ok
+(1 row)
+
+SELECT * FROM subtransaction_tbl;
+ i
+---
+ 1
+ 2
+ 1
+ 2
+(4 rows)
+
+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();
+ERROR: ValueError: this subtransaction has not been entered
+CONTEXT: Traceback (most recent call last):
+ PL/Python function "subtransaction_exit_without_enter", line 2, in <module>
+ plpy.subtransaction().__exit__(None, None, None)
+PL/Python function "subtransaction_exit_without_enter"
+SELECT subtransaction_enter_without_exit();
+WARNING: forcibly aborting a subtransaction that has not been exited
+ subtransaction_enter_without_exit
+-----------------------------------
+
+(1 row)
+
+SELECT subtransaction_exit_twice();
+WARNING: forcibly aborting a subtransaction that has not been exited
+ERROR: ValueError: this subtransaction has not been entered
+CONTEXT: Traceback (most recent call last):
+ PL/Python function "subtransaction_exit_twice", line 3, in <module>
+ plpy.subtransaction().__exit__(None, None, None)
+PL/Python function "subtransaction_exit_twice"
+SELECT subtransaction_enter_twice();
+WARNING: forcibly aborting a subtransaction that has not been exited
+WARNING: forcibly aborting a subtransaction that has not been exited
+ subtransaction_enter_twice
+----------------------------
+
+(1 row)
+
+SELECT subtransaction_exit_same_subtransaction_twice();
+ERROR: ValueError: this subtransaction has already been exited
+CONTEXT: Traceback (most recent call last):
+ PL/Python function "subtransaction_exit_same_subtransaction_twice", line 5, in <module>
+ s.__exit__(None, None, None)
+PL/Python function "subtransaction_exit_same_subtransaction_twice"
+SELECT subtransaction_enter_same_subtransaction_twice();
+WARNING: forcibly aborting a subtransaction that has not been exited
+ERROR: ValueError: this subtransaction has already been entered
+CONTEXT: Traceback (most recent call last):
+ PL/Python function "subtransaction_enter_same_subtransaction_twice", line 4, in <module>
+ s.__enter__()
+PL/Python function "subtransaction_enter_same_subtransaction_twice"
+SELECT subtransaction_enter_subtransaction_in_with();
+ERROR: ValueError: this subtransaction has already been entered
+CONTEXT: Traceback (most recent call last):
+ PL/Python function "subtransaction_enter_subtransaction_in_with", line 3, in <module>
+ s.__enter__()
+PL/Python function "subtransaction_enter_subtransaction_in_with"
+SELECT subtransaction_exit_subtransaction_in_with();
+ERROR: ValueError: this subtransaction has already been exited
+CONTEXT: Traceback (most recent call last):
+ PL/Python function "subtransaction_exit_subtransaction_in_with", line 6, in <module>
+ raise ValueError(e)
+PL/Python function "subtransaction_exit_subtransaction_in_with"
+-- Make sure we don't get a "current transaction is aborted" error
+SELECT 1 as test;
+ test
+------
+ 1
+(1 row)
+
+-- 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();
+WARNING: Caught a SPI error from an explicit subtransaction
+WARNING: Caught a SPI error
+ subtransaction_mix_explicit_and_implicit
+------------------------------------------
+
+(1 row)
+
+SELECT * FROM subtransaction_tbl;
+ i
+---
+ 1
+ 2
+(2 rows)
+
+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();
+ subtransaction_alternative_names
+----------------------------------
+
+(1 row)
+
+-- 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();
+NOTICE: caught
+ try_catch_inside_subtransaction
+---------------------------------
+
+(1 row)
+
+SELECT * FROM subtransaction_tbl;
+ i
+---
+ 1
+(1 row)
+
+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();
+NOTICE: caught
+ pk_violation_inside_subtransaction
+------------------------------------
+
+(1 row)
+
+SELECT * FROM subtransaction_tbl;
+ i
+---
+ 1
+(1 row)
+
+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();
+ cursor_in_subxact
+-------------------
+ 16
+(1 row)
+
+SELECT cursor_aborted_subxact();
+ERROR: ValueError: iterating a cursor in an aborted subtransaction
+CONTEXT: Traceback (most recent call last):
+ PL/Python function "cursor_aborted_subxact", line 8, in <module>
+ fetched = cur.fetch(10)
+PL/Python function "cursor_aborted_subxact"
+SELECT cursor_plan_aborted_subxact();
+ERROR: ValueError: iterating a cursor in an aborted subtransaction
+CONTEXT: Traceback (most recent call last):
+ PL/Python function "cursor_plan_aborted_subxact", line 10, in <module>
+ fetched = cur.fetch(5)
+PL/Python function "cursor_plan_aborted_subxact"
+SELECT cursor_close_aborted_subxact();
+ERROR: ValueError: closing a cursor in an aborted subtransaction
+CONTEXT: Traceback (most recent call last):
+ PL/Python function "cursor_close_aborted_subxact", line 7, in <module>
+ cur.close()
+PL/Python function "cursor_close_aborted_subxact"
diff --git a/src/pl/plpython/expected/plpython_test.out b/src/pl/plpython/expected/plpython_test.out
new file mode 100644
index 0000000..13c1411
--- /dev/null
+++ b/src/pl/plpython/expected/plpython_test.out
@@ -0,0 +1,93 @@
+-- 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();
+ stupid
+--------
+ zarkon
+(1 row)
+
+-- check 2/3 versioning
+CREATE FUNCTION stupidn() RETURNS text AS 'return "zarkon"' LANGUAGE plpython3u;
+select stupidn();
+ stupidn
+---------
+ zarkon
+(1 row)
+
+-- 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;
+ Argument test #1
+-----------------------------------------------------------------------
+ jane doe => {fname: jane, lname: doe, userid: 1, username: j_doe}
+ john doe => {fname: john, lname: doe, userid: 2, username: johnd}
+ willem doe => {fname: willem, lname: doe, userid: 3, username: w_doe}
+(3 rows)
+
+-- 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();
+ module_contents
+-----------------
+ Error
+ Fatal
+ SPIError
+ commit
+ cursor
+ debug
+ error
+ execute
+ fatal
+ info
+ log
+ notice
+ prepare
+ quote_ident
+ quote_literal
+ quote_nullable
+ rollback
+ spiexceptions
+ subtransaction
+ warning
+(20 rows)
+
+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();
+INFO: info
+INFO: 37
+INFO: ()
+INFO: ('info', 37, [1, 2, 3])
+NOTICE: notice
+WARNING: warning
+ERROR: plpy.Error: error
+CONTEXT: Traceback (most recent call last):
+ PL/Python function "elog_test_basic", line 10, in <module>
+ plpy.error('error')
+PL/Python function "elog_test_basic"
diff --git a/src/pl/plpython/expected/plpython_transaction.out b/src/pl/plpython/expected/plpython_transaction.out
new file mode 100644
index 0000000..659ccef
--- /dev/null
+++ b/src/pl/plpython/expected/plpython_transaction.out
@@ -0,0 +1,250 @@
+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;
+ a | b
+---+---
+ 0 |
+ 2 |
+ 4 |
+ 6 |
+ 8 |
+(5 rows)
+
+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;
+ a | b
+---+---
+ 0 |
+ 2 |
+ 4 |
+ 6 |
+ 8 |
+(5 rows)
+
+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();
+ERROR: spiexceptions.InvalidTransactionTermination: invalid transaction termination
+CONTEXT: Traceback (most recent call last):
+ PL/Python function "transaction_test2", line 5, in <module>
+ plpy.commit()
+PL/Python function "transaction_test2"
+SELECT * FROM test1;
+ a | b
+---+---
+(0 rows)
+
+-- 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();
+ERROR: spiexceptions.InvalidTransactionTermination: spiexceptions.InvalidTransactionTermination: invalid transaction termination
+CONTEXT: Traceback (most recent call last):
+ PL/Python function "transaction_test3", line 2, in <module>
+ plpy.execute("CALL transaction_test1()")
+PL/Python function "transaction_test3"
+SELECT * FROM test1;
+ a | b
+---+---
+(0 rows)
+
+-- 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();
+ERROR: spiexceptions.InvalidTransactionTermination: spiexceptions.InvalidTransactionTermination: invalid transaction termination
+CONTEXT: Traceback (most recent call last):
+ PL/Python function "transaction_test4", line 2, in <module>
+ plpy.execute("DO LANGUAGE plpython3u $x$ plpy.commit() $x$")
+PL/Python function "transaction_test4"
+-- commit inside subtransaction (prohibited)
+DO LANGUAGE plpython3u $$
+s = plpy.subtransaction()
+s.enter()
+plpy.commit()
+$$;
+WARNING: forcibly aborting a subtransaction that has not been exited
+ERROR: spiexceptions.InvalidTransactionTermination: cannot commit while a subtransaction is active
+CONTEXT: Traceback (most recent call last):
+ PL/Python anonymous code block, line 4, in <module>
+ plpy.commit()
+PL/Python anonymous code block
+-- 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;
+ a | b
+---+---
+ 0 |
+ 1 |
+ 2 |
+ 3 |
+ 4 |
+(5 rows)
+
+-- check that this doesn't leak a holdable portal
+SELECT * FROM pg_cursors;
+ name | statement | is_holdable | is_binary | is_scrollable | creation_time
+------+-----------+-------------+-----------+---------------+---------------
+(0 rows)
+
+-- 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()
+$$;
+ERROR: spiexceptions.DivisionByZero: division by zero
+CONTEXT: Traceback (most recent call last):
+ PL/Python anonymous code block, line 3, in <module>
+ plpy.execute("INSERT INTO test1 (a) VALUES (12/(%s-2))" % row['x'])
+PL/Python anonymous code block
+SELECT * FROM test1;
+ a | b
+-----+---
+ -6 |
+ -12 |
+(2 rows)
+
+SELECT * FROM pg_cursors;
+ name | statement | is_holdable | is_binary | is_scrollable | creation_time
+------+-----------+-------------+-----------+---------------+---------------
+(0 rows)
+
+-- 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;
+ a | b
+---+---
+(0 rows)
+
+SELECT * FROM pg_cursors;
+ name | statement | is_holdable | is_binary | is_scrollable | creation_time
+------+-----------+-------------+-----------+---------------+---------------
+(0 rows)
+
+-- 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;
+ a | b
+---+---
+ 0 |
+ 2 |
+ 4 |
+(3 rows)
+
+SELECT * FROM pg_cursors;
+ name | statement | is_holdable | is_binary | is_scrollable | creation_time
+------+-----------+-------------+-----------+---------------+---------------
+(0 rows)
+
+-- 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')
+$$;
+ERROR: spiexceptions.ForeignKeyViolation: insert or update on table "testfk" violates foreign key constraint "testfk_f1_fkey"
+DETAIL: Key (f1)=(0) is not present in table "testpk".
+CONTEXT: Traceback (most recent call last):
+ PL/Python anonymous code block, line 4, in <module>
+ plpy.commit()
+PL/Python anonymous code block
+SELECT * FROM testpk;
+ id
+----
+(0 rows)
+
+SELECT * FROM testfk;
+ f1
+----
+(0 rows)
+
+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)")
+$$;
+INFO: sqlstate: 23503
+SELECT * FROM testpk;
+ id
+----
+ 1
+(1 row)
+
+SELECT * FROM testfk;
+ f1
+----
+ 1
+(1 row)
+
+DROP TABLE test1;
+DROP TABLE test2;
diff --git a/src/pl/plpython/expected/plpython_trigger.out b/src/pl/plpython/expected/plpython_trigger.out
new file mode 100644
index 0000000..dd1ca32
--- /dev/null
+++ b/src/pl/plpython/expected/plpython_trigger.out
@@ -0,0 +1,620 @@
+-- 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;
+ fname | lname | username | userid
+--------+-------+----------+--------
+ jane | doe | j_doe | 1
+ john | doe | johnd | 2
+ willem | doe | w_doe | 3
+ rick | smith | slash | 4
+(4 rows)
+
+-- 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;
+ fname | lname | username | userid
+---------+--------+----------+--------
+ jane | doe | j_doe | 1
+ john | doe | johnd | 2
+ willem | doe | w_doe | 3
+ rick | smith | slash | 4
+ willem | smith | w_smith | 5
+ charles | darwin | beagle | 6
+(6 rows)
+
+-- 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');
+NOTICE: TD[args] => ['23', 'skidoo']
+NOTICE: TD[event] => INSERT
+NOTICE: TD[level] => STATEMENT
+NOTICE: TD[name] => show_trigger_data_trig_stmt
+NOTICE: TD[new] => None
+NOTICE: TD[old] => None
+NOTICE: TD[relid] => bogus:12345
+NOTICE: TD[table_name] => trigger_test
+NOTICE: TD[table_schema] => public
+NOTICE: TD[when] => BEFORE
+NOTICE: TD[args] => ['23', 'skidoo']
+NOTICE: TD[event] => INSERT
+NOTICE: TD[level] => ROW
+NOTICE: TD[name] => show_trigger_data_trig_before
+NOTICE: TD[new] => {'i': 1, 'v': 'insert'}
+NOTICE: TD[old] => None
+NOTICE: TD[relid] => bogus:12345
+NOTICE: TD[table_name] => trigger_test
+NOTICE: TD[table_schema] => public
+NOTICE: TD[when] => BEFORE
+NOTICE: TD[args] => ['23', 'skidoo']
+NOTICE: TD[event] => INSERT
+NOTICE: TD[level] => ROW
+NOTICE: TD[name] => show_trigger_data_trig_after
+NOTICE: TD[new] => {'i': 1, 'v': 'insert'}
+NOTICE: TD[old] => None
+NOTICE: TD[relid] => bogus:12345
+NOTICE: TD[table_name] => trigger_test
+NOTICE: TD[table_schema] => public
+NOTICE: TD[when] => AFTER
+update trigger_test set v = 'update' where i = 1;
+NOTICE: TD[args] => ['23', 'skidoo']
+NOTICE: TD[event] => UPDATE
+NOTICE: TD[level] => STATEMENT
+NOTICE: TD[name] => show_trigger_data_trig_stmt
+NOTICE: TD[new] => None
+NOTICE: TD[old] => None
+NOTICE: TD[relid] => bogus:12345
+NOTICE: TD[table_name] => trigger_test
+NOTICE: TD[table_schema] => public
+NOTICE: TD[when] => BEFORE
+NOTICE: TD[args] => ['23', 'skidoo']
+NOTICE: TD[event] => UPDATE
+NOTICE: TD[level] => ROW
+NOTICE: TD[name] => show_trigger_data_trig_before
+NOTICE: TD[new] => {'i': 1, 'v': 'update'}
+NOTICE: TD[old] => {'i': 1, 'v': 'insert'}
+NOTICE: TD[relid] => bogus:12345
+NOTICE: TD[table_name] => trigger_test
+NOTICE: TD[table_schema] => public
+NOTICE: TD[when] => BEFORE
+NOTICE: TD[args] => ['23', 'skidoo']
+NOTICE: TD[event] => UPDATE
+NOTICE: TD[level] => ROW
+NOTICE: TD[name] => show_trigger_data_trig_after
+NOTICE: TD[new] => {'i': 1, 'v': 'update'}
+NOTICE: TD[old] => {'i': 1, 'v': 'insert'}
+NOTICE: TD[relid] => bogus:12345
+NOTICE: TD[table_name] => trigger_test
+NOTICE: TD[table_schema] => public
+NOTICE: TD[when] => AFTER
+delete from trigger_test;
+NOTICE: TD[args] => ['23', 'skidoo']
+NOTICE: TD[event] => DELETE
+NOTICE: TD[level] => STATEMENT
+NOTICE: TD[name] => show_trigger_data_trig_stmt
+NOTICE: TD[new] => None
+NOTICE: TD[old] => None
+NOTICE: TD[relid] => bogus:12345
+NOTICE: TD[table_name] => trigger_test
+NOTICE: TD[table_schema] => public
+NOTICE: TD[when] => BEFORE
+NOTICE: TD[args] => ['23', 'skidoo']
+NOTICE: TD[event] => DELETE
+NOTICE: TD[level] => ROW
+NOTICE: TD[name] => show_trigger_data_trig_before
+NOTICE: TD[new] => None
+NOTICE: TD[old] => {'i': 1, 'v': 'update'}
+NOTICE: TD[relid] => bogus:12345
+NOTICE: TD[table_name] => trigger_test
+NOTICE: TD[table_schema] => public
+NOTICE: TD[when] => BEFORE
+NOTICE: TD[args] => ['23', 'skidoo']
+NOTICE: TD[event] => DELETE
+NOTICE: TD[level] => ROW
+NOTICE: TD[name] => show_trigger_data_trig_after
+NOTICE: TD[new] => None
+NOTICE: TD[old] => {'i': 1, 'v': 'update'}
+NOTICE: TD[relid] => bogus:12345
+NOTICE: TD[table_name] => trigger_test
+NOTICE: TD[table_schema] => public
+NOTICE: TD[when] => AFTER
+truncate table trigger_test;
+NOTICE: TD[args] => ['23', 'skidoo']
+NOTICE: TD[event] => TRUNCATE
+NOTICE: TD[level] => STATEMENT
+NOTICE: TD[name] => show_trigger_data_trig_stmt
+NOTICE: TD[new] => None
+NOTICE: TD[old] => None
+NOTICE: TD[relid] => bogus:12345
+NOTICE: TD[table_name] => trigger_test
+NOTICE: TD[table_schema] => public
+NOTICE: TD[when] => BEFORE
+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);
+NOTICE: TD[args] => None
+NOTICE: TD[event] => INSERT
+NOTICE: TD[level] => ROW
+NOTICE: TD[name] => show_trigger_data_trig_before
+NOTICE: TD[new] => {'i': 1}
+NOTICE: TD[old] => None
+NOTICE: TD[relid] => bogus:12345
+NOTICE: TD[table_name] => trigger_test_generated
+NOTICE: TD[table_schema] => public
+NOTICE: TD[when] => BEFORE
+NOTICE: TD[args] => None
+NOTICE: TD[event] => INSERT
+NOTICE: TD[level] => ROW
+NOTICE: TD[name] => show_trigger_data_trig_after
+NOTICE: TD[new] => {'i': 1, 'j': 2}
+NOTICE: TD[old] => None
+NOTICE: TD[relid] => bogus:12345
+NOTICE: TD[table_name] => trigger_test_generated
+NOTICE: TD[table_schema] => public
+NOTICE: TD[when] => AFTER
+update trigger_test_generated set i = 11 where i = 1;
+NOTICE: TD[args] => None
+NOTICE: TD[event] => UPDATE
+NOTICE: TD[level] => ROW
+NOTICE: TD[name] => show_trigger_data_trig_before
+NOTICE: TD[new] => {'i': 11}
+NOTICE: TD[old] => {'i': 1, 'j': 2}
+NOTICE: TD[relid] => bogus:12345
+NOTICE: TD[table_name] => trigger_test_generated
+NOTICE: TD[table_schema] => public
+NOTICE: TD[when] => BEFORE
+NOTICE: TD[args] => None
+NOTICE: TD[event] => UPDATE
+NOTICE: TD[level] => ROW
+NOTICE: TD[name] => show_trigger_data_trig_after
+NOTICE: TD[new] => {'i': 11, 'j': 22}
+NOTICE: TD[old] => {'i': 1, 'j': 2}
+NOTICE: TD[relid] => bogus:12345
+NOTICE: TD[table_name] => trigger_test_generated
+NOTICE: TD[table_schema] => public
+NOTICE: TD[when] => AFTER
+delete from trigger_test_generated;
+NOTICE: TD[args] => None
+NOTICE: TD[event] => DELETE
+NOTICE: TD[level] => ROW
+NOTICE: TD[name] => show_trigger_data_trig_before
+NOTICE: TD[new] => None
+NOTICE: TD[old] => {'i': 11, 'j': 22}
+NOTICE: TD[relid] => bogus:12345
+NOTICE: TD[table_name] => trigger_test_generated
+NOTICE: TD[table_schema] => public
+NOTICE: TD[when] => BEFORE
+NOTICE: TD[args] => None
+NOTICE: TD[event] => DELETE
+NOTICE: TD[level] => ROW
+NOTICE: TD[name] => show_trigger_data_trig_after
+NOTICE: TD[new] => None
+NOTICE: TD[old] => {'i': 11, 'j': 22}
+NOTICE: TD[relid] => bogus:12345
+NOTICE: TD[table_name] => trigger_test_generated
+NOTICE: TD[table_schema] => public
+NOTICE: TD[when] => AFTER
+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');
+NOTICE: TD[args] => ['24', 'skidoo view']
+NOTICE: TD[event] => INSERT
+NOTICE: TD[level] => ROW
+NOTICE: TD[name] => show_trigger_data_trig
+NOTICE: TD[new] => {'i': 2, 'v': 'insert'}
+NOTICE: TD[old] => None
+NOTICE: TD[relid] => bogus:12345
+NOTICE: TD[table_name] => trigger_test_view
+NOTICE: TD[table_schema] => public
+NOTICE: TD[when] => INSTEAD OF
+update trigger_test_view set v = 'update' where i = 1;
+NOTICE: TD[args] => ['24', 'skidoo view']
+NOTICE: TD[event] => UPDATE
+NOTICE: TD[level] => ROW
+NOTICE: TD[name] => show_trigger_data_trig
+NOTICE: TD[new] => {'i': 1, 'v': 'update'}
+NOTICE: TD[old] => {'i': 1, 'v': 'insert'}
+NOTICE: TD[relid] => bogus:12345
+NOTICE: TD[table_name] => trigger_test_view
+NOTICE: TD[table_schema] => public
+NOTICE: TD[when] => INSTEAD OF
+delete from trigger_test_view;
+NOTICE: TD[args] => ['24', 'skidoo view']
+NOTICE: TD[event] => DELETE
+NOTICE: TD[level] => ROW
+NOTICE: TD[name] => show_trigger_data_trig
+NOTICE: TD[new] => None
+NOTICE: TD[old] => {'i': 1, 'v': 'insert'}
+NOTICE: TD[relid] => bogus:12345
+NOTICE: TD[table_name] => trigger_test_view
+NOTICE: TD[table_schema] => public
+NOTICE: TD[when] => INSTEAD OF
+DROP FUNCTION trigger_data() CASCADE;
+NOTICE: drop cascades to trigger show_trigger_data_trig on view trigger_test_view
+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');
+ERROR: unexpected return value from trigger procedure
+DETAIL: Expected None or a string.
+CONTEXT: PL/Python function "stupid1"
+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;
+WARNING: PL/Python trigger function returned "MODIFY" in a DELETE trigger -- ignored
+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;
+ERROR: unexpected return value from trigger procedure
+DETAIL: Expected None, "OK", "SKIP", or "MODIFY".
+CONTEXT: PL/Python function "stupid3"
+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;
+ERROR: unexpected return value from trigger procedure
+DETAIL: Expected None, "OK", "SKIP", or "MODIFY".
+CONTEXT: PL/Python function "stupid3u"
+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;
+ERROR: TD["new"] deleted, cannot modify row
+CONTEXT: while modifying trigger row
+PL/Python function "stupid4"
+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;
+ERROR: TD["new"] is not a dictionary
+CONTEXT: while modifying trigger row
+PL/Python function "stupid5"
+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;
+ERROR: TD["new"] dictionary key at ordinal position 0 is not a string
+CONTEXT: while modifying trigger row
+PL/Python function "stupid6"
+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;
+ERROR: key "a" found in TD["new"] does not exist as a column in the triggering row
+CONTEXT: while modifying trigger row
+PL/Python function "stupid7"
+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;
+ERROR: key "a" found in TD["new"] does not exist as a column in the triggering row
+CONTEXT: while modifying trigger row
+PL/Python function "stupid7u"
+DROP TRIGGER stupid_trigger7 ON trigger_test;
+-- calling a trigger function directly
+SELECT stupid7();
+ERROR: trigger functions can only be called as triggers
+--
+-- Null values
+--
+SELECT * FROM trigger_test;
+ i | v
+---+------
+ 0 | zero
+(1 row)
+
+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;
+ i | v
+---+---
+ 0 |
+(1 row)
+
+--
+-- 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;
+ a | modif_time
+---+---------------------
+ a | 2010-10-09 21:57:34
+(1 row)
+
+UPDATE pb SET a = 'b';
+SELECT * FROM pb;
+ a | modif_time
+---+---------------------
+ b | 2010-10-13 21:57:29
+(1 row)
+
+-- triggers for tables with composite types
+CREATE TABLE comp1 (i integer, j boolean);
+CREATE TYPE comp2 AS (k integer, l boolean);
+CREATE TABLE composite_trigger_test (f1 comp1, f2 comp2);
+CREATE FUNCTION composite_trigger_f() RETURNS trigger AS $$
+ TD['new']['f1'] = (3, False)
+ TD['new']['f2'] = {'k': 7, 'l': 'yes', 'ignored': 10}
+ return 'MODIFY'
+$$ LANGUAGE 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;
+ f1 | f2
+-------+-------
+ (3,f) | (7,t)
+(1 row)
+
+-- 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;
+ f1 | f2
+-------+-------
+ |
+ (1,f) |
+ (,t) | (1,f)
+(3 rows)
+
+-- 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;
+ c
+-------------------
+
+ ("(1,f)",,3)
+ ("(,t)","(1,f)",)
+(3 rows)
+
+-- 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;
+ data
+------
+ 1234
+(1 row)
+
+DROP TABLE a;
+INSERT INTO b DEFAULT VALUES;
+SELECT * FROM b;
+ data
+------
+ 1234
+(1 row)
+
+-- 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';
+INFO: old: 1 -> a
+INFO: new: 1 -> 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);
+ERROR: cannot set generated column "j"
+CONTEXT: while modifying trigger row
+PL/Python function "generated_test_func1"
+SELECT * FROM trigger_test_generated;
+ i | j
+---+---
+(0 rows)
+
diff --git a/src/pl/plpython/expected/plpython_types.out b/src/pl/plpython/expected/plpython_types.out
new file mode 100644
index 0000000..8a680e1
--- /dev/null
+++ b/src/pl/plpython/expected/plpython_types.out
@@ -0,0 +1,1069 @@
+--
+-- 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);
+INFO: (True, <class 'bool'>)
+ test_type_conversion_bool
+---------------------------
+ t
+(1 row)
+
+SELECT * FROM test_type_conversion_bool(false);
+INFO: (False, <class 'bool'>)
+ test_type_conversion_bool
+---------------------------
+ f
+(1 row)
+
+SELECT * FROM test_type_conversion_bool(null);
+INFO: (None, <class 'NoneType'>)
+ test_type_conversion_bool
+---------------------------
+
+(1 row)
+
+-- 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);
+INFO: (0, False)
+ test_type_conversion_bool_other
+---------------------------------
+ f
+(1 row)
+
+SELECT * FROM test_type_conversion_bool_other(1);
+INFO: (5, True)
+ test_type_conversion_bool_other
+---------------------------------
+ t
+(1 row)
+
+SELECT * FROM test_type_conversion_bool_other(2);
+INFO: ('', False)
+ test_type_conversion_bool_other
+---------------------------------
+ f
+(1 row)
+
+SELECT * FROM test_type_conversion_bool_other(3);
+INFO: ('fa', True)
+ test_type_conversion_bool_other
+---------------------------------
+ t
+(1 row)
+
+SELECT * FROM test_type_conversion_bool_other(4);
+INFO: ([], False)
+ test_type_conversion_bool_other
+---------------------------------
+ f
+(1 row)
+
+SELECT * FROM test_type_conversion_bool_other(5);
+INFO: ([0], True)
+ test_type_conversion_bool_other
+---------------------------------
+ t
+(1 row)
+
+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');
+INFO: ('a', <class 'str'>)
+ test_type_conversion_char
+---------------------------
+ a
+(1 row)
+
+SELECT * FROM test_type_conversion_char(null);
+INFO: (None, <class 'NoneType'>)
+ test_type_conversion_char
+---------------------------
+
+(1 row)
+
+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);
+INFO: (100, <class 'int'>)
+ test_type_conversion_int2
+---------------------------
+ 100
+(1 row)
+
+SELECT * FROM test_type_conversion_int2(-100::int2);
+INFO: (-100, <class 'int'>)
+ test_type_conversion_int2
+---------------------------
+ -100
+(1 row)
+
+SELECT * FROM test_type_conversion_int2(null);
+INFO: (None, <class 'NoneType'>)
+ test_type_conversion_int2
+---------------------------
+
+(1 row)
+
+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);
+INFO: (100, <class 'int'>)
+ test_type_conversion_int4
+---------------------------
+ 100
+(1 row)
+
+SELECT * FROM test_type_conversion_int4(-100);
+INFO: (-100, <class 'int'>)
+ test_type_conversion_int4
+---------------------------
+ -100
+(1 row)
+
+SELECT * FROM test_type_conversion_int4(null);
+INFO: (None, <class 'NoneType'>)
+ test_type_conversion_int4
+---------------------------
+
+(1 row)
+
+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);
+INFO: (100, <class 'int'>)
+ test_type_conversion_int8
+---------------------------
+ 100
+(1 row)
+
+SELECT * FROM test_type_conversion_int8(-100);
+INFO: (-100, <class 'int'>)
+ test_type_conversion_int8
+---------------------------
+ -100
+(1 row)
+
+SELECT * FROM test_type_conversion_int8(5000000000);
+INFO: (5000000000, <class 'int'>)
+ test_type_conversion_int8
+---------------------------
+ 5000000000
+(1 row)
+
+SELECT * FROM test_type_conversion_int8(null);
+INFO: (None, <class 'NoneType'>)
+ test_type_conversion_int8
+---------------------------
+
+(1 row)
+
+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);
+INFO: ('100', 'Decimal')
+ test_type_conversion_numeric
+------------------------------
+ 100
+(1 row)
+
+SELECT * FROM test_type_conversion_numeric(-100);
+INFO: ('-100', 'Decimal')
+ test_type_conversion_numeric
+------------------------------
+ -100
+(1 row)
+
+SELECT * FROM test_type_conversion_numeric(100.0);
+INFO: ('100.0', 'Decimal')
+ test_type_conversion_numeric
+------------------------------
+ 100.0
+(1 row)
+
+SELECT * FROM test_type_conversion_numeric(100.00);
+INFO: ('100.00', 'Decimal')
+ test_type_conversion_numeric
+------------------------------
+ 100.00
+(1 row)
+
+SELECT * FROM test_type_conversion_numeric(5000000000.5);
+INFO: ('5000000000.5', 'Decimal')
+ test_type_conversion_numeric
+------------------------------
+ 5000000000.5
+(1 row)
+
+SELECT * FROM test_type_conversion_numeric(1234567890.0987654321);
+INFO: ('1234567890.0987654321', 'Decimal')
+ test_type_conversion_numeric
+------------------------------
+ 1234567890.0987654321
+(1 row)
+
+SELECT * FROM test_type_conversion_numeric(-1234567890.0987654321);
+INFO: ('-1234567890.0987654321', 'Decimal')
+ test_type_conversion_numeric
+------------------------------
+ -1234567890.0987654321
+(1 row)
+
+SELECT * FROM test_type_conversion_numeric(null);
+INFO: ('None', 'NoneType')
+ test_type_conversion_numeric
+------------------------------
+
+(1 row)
+
+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);
+INFO: (100.0, <class 'float'>)
+ test_type_conversion_float4
+-----------------------------
+ 100
+(1 row)
+
+SELECT * FROM test_type_conversion_float4(-100);
+INFO: (-100.0, <class 'float'>)
+ test_type_conversion_float4
+-----------------------------
+ -100
+(1 row)
+
+SELECT * FROM test_type_conversion_float4(5000.5);
+INFO: (5000.5, <class 'float'>)
+ test_type_conversion_float4
+-----------------------------
+ 5000.5
+(1 row)
+
+SELECT * FROM test_type_conversion_float4(null);
+INFO: (None, <class 'NoneType'>)
+ test_type_conversion_float4
+-----------------------------
+
+(1 row)
+
+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);
+INFO: (100.0, <class 'float'>)
+ test_type_conversion_float8
+-----------------------------
+ 100
+(1 row)
+
+SELECT * FROM test_type_conversion_float8(-100);
+INFO: (-100.0, <class 'float'>)
+ test_type_conversion_float8
+-----------------------------
+ -100
+(1 row)
+
+SELECT * FROM test_type_conversion_float8(5000000000.5);
+INFO: (5000000000.5, <class 'float'>)
+ test_type_conversion_float8
+-----------------------------
+ 5000000000.5
+(1 row)
+
+SELECT * FROM test_type_conversion_float8(null);
+INFO: (None, <class 'NoneType'>)
+ test_type_conversion_float8
+-----------------------------
+
+(1 row)
+
+SELECT * FROM test_type_conversion_float8(100100100.654321);
+INFO: (100100100.654321, <class 'float'>)
+ test_type_conversion_float8
+-----------------------------
+ 100100100.654321
+(1 row)
+
+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);
+INFO: (100, <class 'int'>)
+ test_type_conversion_oid
+--------------------------
+ 100
+(1 row)
+
+SELECT * FROM test_type_conversion_oid(2147483649);
+INFO: (2147483649, <class 'int'>)
+ test_type_conversion_oid
+--------------------------
+ 2147483649
+(1 row)
+
+SELECT * FROM test_type_conversion_oid(null);
+INFO: (None, <class 'NoneType'>)
+ test_type_conversion_oid
+--------------------------
+
+(1 row)
+
+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');
+INFO: ('hello world', <class 'str'>)
+ test_type_conversion_text
+---------------------------
+ hello world
+(1 row)
+
+SELECT * FROM test_type_conversion_text(null);
+INFO: (None, <class 'NoneType'>)
+ test_type_conversion_text
+---------------------------
+
+(1 row)
+
+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');
+INFO: (b'hello world', <class 'bytes'>)
+ test_type_conversion_bytea
+----------------------------
+ \x68656c6c6f20776f726c64
+(1 row)
+
+SELECT * FROM test_type_conversion_bytea(E'null\\000byte');
+INFO: (b'null\x00byte', <class 'bytes'>)
+ test_type_conversion_bytea
+----------------------------
+ \x6e756c6c0062797465
+(1 row)
+
+SELECT * FROM test_type_conversion_bytea(null);
+INFO: (None, <class 'NoneType'>)
+ test_type_conversion_bytea
+----------------------------
+
+(1 row)
+
+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;
+ test_type_unmarshal
+---------------------
+ hello world
+(1 row)
+
+--
+-- 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);
+ test_type_conversion_booltrue
+-------------------------------
+ t
+(1 row)
+
+SELECT * FROM test_type_conversion_booltrue(false, true);
+ERROR: value for domain booltrue violates check constraint "booltrue_check"
+SELECT * FROM test_type_conversion_booltrue(true, false);
+ERROR: value for domain booltrue violates check constraint "booltrue_check"
+CONTEXT: while creating return value
+PL/Python function "test_type_conversion_booltrue"
+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);
+INFO: (100, <class 'int'>)
+ test_type_conversion_uint2
+----------------------------
+ 50
+(1 row)
+
+SELECT * FROM test_type_conversion_uint2(100::uint2, -50);
+INFO: (100, <class 'int'>)
+ERROR: value for domain uint2 violates check constraint "uint2_check"
+CONTEXT: while creating return value
+PL/Python function "test_type_conversion_uint2"
+SELECT * FROM test_type_conversion_uint2(null, 1);
+INFO: (None, <class 'NoneType'>)
+ test_type_conversion_uint2
+----------------------------
+ 1
+(1 row)
+
+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);
+ test_type_conversion_nnint
+----------------------------
+ 20
+(1 row)
+
+SELECT * FROM test_type_conversion_nnint(null, 20);
+ERROR: value for domain nnint violates check constraint "nnint_check"
+SELECT * FROM test_type_conversion_nnint(10, null);
+ERROR: value for domain nnint violates check constraint "nnint_check"
+CONTEXT: while creating return value
+PL/Python function "test_type_conversion_nnint"
+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');
+INFO: (b'hello wold', <class 'bytes'>)
+ test_type_conversion_bytea10
+------------------------------
+ \x68656c6c6f20776f6c64
+(1 row)
+
+SELECT * FROM test_type_conversion_bytea10('hello world', 'hello wold');
+ERROR: value for domain bytea10 violates check constraint "bytea10_check"
+SELECT * FROM test_type_conversion_bytea10('hello word', 'hello world');
+INFO: (b'hello word', <class 'bytes'>)
+ERROR: value for domain bytea10 violates check constraint "bytea10_check"
+CONTEXT: while creating return value
+PL/Python function "test_type_conversion_bytea10"
+SELECT * FROM test_type_conversion_bytea10(null, 'hello word');
+ERROR: value for domain bytea10 violates check constraint "bytea10_check"
+SELECT * FROM test_type_conversion_bytea10('hello word', null);
+INFO: (b'hello word', <class 'bytes'>)
+ERROR: value for domain bytea10 violates check constraint "bytea10_check"
+CONTEXT: while creating return value
+PL/Python function "test_type_conversion_bytea10"
+--
+-- 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]);
+INFO: ([0, 100], <class 'list'>)
+ test_type_conversion_array_int4
+---------------------------------
+ {0,100}
+(1 row)
+
+SELECT * FROM test_type_conversion_array_int4(ARRAY[0,-100,55]);
+INFO: ([0, -100, 55], <class 'list'>)
+ test_type_conversion_array_int4
+---------------------------------
+ {0,-100,55}
+(1 row)
+
+SELECT * FROM test_type_conversion_array_int4(ARRAY[NULL,1]);
+INFO: ([None, 1], <class 'list'>)
+ test_type_conversion_array_int4
+---------------------------------
+ {NULL,1}
+(1 row)
+
+SELECT * FROM test_type_conversion_array_int4(ARRAY[]::integer[]);
+INFO: ([], <class 'list'>)
+ test_type_conversion_array_int4
+---------------------------------
+ {}
+(1 row)
+
+SELECT * FROM test_type_conversion_array_int4(NULL);
+INFO: (None, <class 'NoneType'>)
+ test_type_conversion_array_int4
+---------------------------------
+
+(1 row)
+
+SELECT * FROM test_type_conversion_array_int4(ARRAY[[1,2,3],[4,5,6]]);
+INFO: ([[1, 2, 3], [4, 5, 6]], <class 'list'>)
+ test_type_conversion_array_int4
+---------------------------------
+ {{1,2,3},{4,5,6}}
+(1 row)
+
+SELECT * FROM test_type_conversion_array_int4(ARRAY[[[1,2,NULL],[NULL,5,6]],[[NULL,8,9],[10,11,12]]]);
+INFO: ([[[1, 2, None], [None, 5, 6]], [[None, 8, 9], [10, 11, 12]]], <class 'list'>)
+ test_type_conversion_array_int4
+---------------------------------------------------
+ {{{1,2,NULL},{NULL,5,6}},{{NULL,8,9},{10,11,12}}}
+(1 row)
+
+SELECT * FROM test_type_conversion_array_int4('[2:4]={1,2,3}');
+INFO: ([1, 2, 3], <class 'list'>)
+ test_type_conversion_array_int4
+---------------------------------
+ {1,2,3}
+(1 row)
+
+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[]);
+INFO: ([[[1, 2, None], [None, 5, 6]], [[None, 8, 9], [10, 11, 12]]], <class 'list'>)
+ test_type_conversion_array_int8
+---------------------------------------------------
+ {{{1,2,NULL},{NULL,5,6}},{{NULL,8,9},{10,11,12}}}
+(1 row)
+
+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[]);
+INFO: ([[['09-21-2016', '09-22-2016', None], [None, '10-21-2016', '10-22-2016']], [[None, '11-21-2016', '10-21-2016'], ['09-21-2015', '09-22-2015', '09-21-2014']]], <class 'list'>)
+ test_type_conversion_array_date
+---------------------------------------------------------------------------------------------------------------------------------
+ {{{09-21-2016,09-22-2016,NULL},{NULL,10-21-2016,10-22-2016}},{{NULL,11-21-2016,10-21-2016},{09-21-2015,09-22-2015,09-21-2014}}}
+(1 row)
+
+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[]);
+INFO: ([[['Wed Sep 21 15:34:24.078792 2016', 'Sat Oct 22 11:34:24.078795 2016', None], [None, 'Fri Oct 21 11:34:25.078792 2016', 'Fri Oct 21 11:34:24.098792 2016']], [[None, 'Thu Jan 21 11:34:24.078792 2016', 'Mon Nov 21 11:34:24.108792 2016'], ['Mon Sep 21 11:34:24.079792 2015', 'Sun Sep 21 11:34:24.078792 2014', 'Sat Sep 21 11:34:24.078792 2013']]], <class 'list'>)
+ test_type_conversion_array_timestamp
+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ {{{"Wed Sep 21 15:34:24.078792 2016","Sat Oct 22 11:34:24.078795 2016",NULL},{NULL,"Fri Oct 21 11:34:25.078792 2016","Fri Oct 21 11:34:24.098792 2016"}},{{NULL,"Thu Jan 21 11:34:24.078792 2016","Mon Nov 21 11:34:24.108792 2016"},{"Mon Sep 21 11:34:24.079792 2015","Sun Sep 21 11:34:24.078792 2014","Sat Sep 21 11:34:24.078792 2013"}}}
+(1 row)
+
+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);
+INFO: ([[[[0, 1, 2, 3, 4, 5, 6, 7], [0, 1, 2, 3, 4, 5, 6, 7], [0, 1, 2, 3, 4, 5, 6, 7], [0, 1, 2, 3, 4, 5, 6, 7], [0, 1, 2, 3, 4, 5, 6, 7]], [[0, 1, 2, 3, 4, 5, 6, 7], [0, 1, 2, 3, 4, 5, 6, 7], [0, 1, 2, 3, 4, 5, 6, 7], [0, 1, 2, 3, 4, 5, 6, 7], [0, 1, 2, 3, 4, 5, 6, 7]], [[0, 1, 2, 3, 4, 5, 6, 7], [0, 1, 2, 3, 4, 5, 6, 7], [0, 1, 2, 3, 4, 5, 6, 7], [0, 1, 2, 3, 4, 5, 6, 7], [0, 1, 2, 3, 4, 5, 6, 7]]], [[[0, 1, 2, 3, 4, 5, 6, 7], [0, 1, 2, 3, 4, 5, 6, 7], [0, 1, 2, 3, 4, 5, 6, 7], [0, 1, 2, 3, 4, 5, 6, 7], [0, 1, 2, 3, 4, 5, 6, 7]], [[0, 1, 2, 3, 4, 5, 6, 7], [0, 1, 2, 3, 4, 5, 6, 7], [0, 1, 2, 3, 4, 5, 6, 7], [0, 1, 2, 3, 4, 5, 6, 7], [0, 1, 2, 3, 4, 5, 6, 7]], [[0, 1, 2, 3, 4, 5, 6, 7], [0, 1, 2, 3, 4, 5, 6, 7], [0, 1, 2, 3, 4, 5, 6, 7], [0, 1, 2, 3, 4, 5, 6, 7], [0, 1, 2, 3, 4, 5, 6, 7]]]], <class 'list'>)
+ pyreturnmultidemint4
+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ {{{{0,1,2,3,4,5,6,7},{0,1,2,3,4,5,6,7},{0,1,2,3,4,5,6,7},{0,1,2,3,4,5,6,7},{0,1,2,3,4,5,6,7}},{{0,1,2,3,4,5,6,7},{0,1,2,3,4,5,6,7},{0,1,2,3,4,5,6,7},{0,1,2,3,4,5,6,7},{0,1,2,3,4,5,6,7}},{{0,1,2,3,4,5,6,7},{0,1,2,3,4,5,6,7},{0,1,2,3,4,5,6,7},{0,1,2,3,4,5,6,7},{0,1,2,3,4,5,6,7}}},{{{0,1,2,3,4,5,6,7},{0,1,2,3,4,5,6,7},{0,1,2,3,4,5,6,7},{0,1,2,3,4,5,6,7},{0,1,2,3,4,5,6,7}},{{0,1,2,3,4,5,6,7},{0,1,2,3,4,5,6,7},{0,1,2,3,4,5,6,7},{0,1,2,3,4,5,6,7},{0,1,2,3,4,5,6,7}},{{0,1,2,3,4,5,6,7},{0,1,2,3,4,5,6,7},{0,1,2,3,4,5,6,7},{0,1,2,3,4,5,6,7},{0,1,2,3,4,5,6,7}}}}
+(1 row)
+
+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);
+INFO: ([[[[0, 1, 2, 3, 4], [0, 1, 2, 3, 4], [0, 1, 2, 3, 4], [0, 1, 2, 3, 4], [0, 1, 2, 3, 4]], [[0, 1, 2, 3, 4], [0, 1, 2, 3, 4], [0, 1, 2, 3, 4], [0, 1, 2, 3, 4], [0, 1, 2, 3, 4]], [[0, 1, 2, 3, 4], [0, 1, 2, 3, 4], [0, 1, 2, 3, 4], [0, 1, 2, 3, 4], [0, 1, 2, 3, 4]]], [[[0, 1, 2, 3, 4], [0, 1, 2, 3, 4], [0, 1, 2, 3, 4], [0, 1, 2, 3, 4], [0, 1, 2, 3, 4]], [[0, 1, 2, 3, 4], [0, 1, 2, 3, 4], [0, 1, 2, 3, 4], [0, 1, 2, 3, 4], [0, 1, 2, 3, 4]], [[0, 1, 2, 3, 4], [0, 1, 2, 3, 4], [0, 1, 2, 3, 4], [0, 1, 2, 3, 4], [0, 1, 2, 3, 4]]]], <class 'list'>)
+ pyreturnmultidemint8
+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ {{{{0,1,2,3,4},{0,1,2,3,4},{0,1,2,3,4},{0,1,2,3,4},{0,1,2,3,4}},{{0,1,2,3,4},{0,1,2,3,4},{0,1,2,3,4},{0,1,2,3,4},{0,1,2,3,4}},{{0,1,2,3,4},{0,1,2,3,4},{0,1,2,3,4},{0,1,2,3,4},{0,1,2,3,4}}},{{{0,1,2,3,4},{0,1,2,3,4},{0,1,2,3,4},{0,1,2,3,4},{0,1,2,3,4}},{{0,1,2,3,4},{0,1,2,3,4},{0,1,2,3,4},{0,1,2,3,4},{0,1,2,3,4}},{{0,1,2,3,4},{0,1,2,3,4},{0,1,2,3,4},{0,1,2,3,4},{0,1,2,3,4}}}}
+(1 row)
+
+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);
+INFO: ([[[[0, 1, 2, 3, 4, 5], [0, 1, 2, 3, 4, 5], [0, 1, 2, 3, 4, 5], [0, 1, 2, 3, 4, 5], [0, 1, 2, 3, 4, 5]], [[0, 1, 2, 3, 4, 5], [0, 1, 2, 3, 4, 5], [0, 1, 2, 3, 4, 5], [0, 1, 2, 3, 4, 5], [0, 1, 2, 3, 4, 5]], [[0, 1, 2, 3, 4, 5], [0, 1, 2, 3, 4, 5], [0, 1, 2, 3, 4, 5], [0, 1, 2, 3, 4, 5], [0, 1, 2, 3, 4, 5]]], [[[0, 1, 2, 3, 4, 5], [0, 1, 2, 3, 4, 5], [0, 1, 2, 3, 4, 5], [0, 1, 2, 3, 4, 5], [0, 1, 2, 3, 4, 5]], [[0, 1, 2, 3, 4, 5], [0, 1, 2, 3, 4, 5], [0, 1, 2, 3, 4, 5], [0, 1, 2, 3, 4, 5], [0, 1, 2, 3, 4, 5]], [[0, 1, 2, 3, 4, 5], [0, 1, 2, 3, 4, 5], [0, 1, 2, 3, 4, 5], [0, 1, 2, 3, 4, 5], [0, 1, 2, 3, 4, 5]]]], <class 'list'>)
+ pyreturnmultidemfloat4
+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ {{{{0,1,2,3,4,5},{0,1,2,3,4,5},{0,1,2,3,4,5},{0,1,2,3,4,5},{0,1,2,3,4,5}},{{0,1,2,3,4,5},{0,1,2,3,4,5},{0,1,2,3,4,5},{0,1,2,3,4,5},{0,1,2,3,4,5}},{{0,1,2,3,4,5},{0,1,2,3,4,5},{0,1,2,3,4,5},{0,1,2,3,4,5},{0,1,2,3,4,5}}},{{{0,1,2,3,4,5},{0,1,2,3,4,5},{0,1,2,3,4,5},{0,1,2,3,4,5},{0,1,2,3,4,5}},{{0,1,2,3,4,5},{0,1,2,3,4,5},{0,1,2,3,4,5},{0,1,2,3,4,5},{0,1,2,3,4,5}},{{0,1,2,3,4,5},{0,1,2,3,4,5},{0,1,2,3,4,5},{0,1,2,3,4,5},{0,1,2,3,4,5}}}}
+(1 row)
+
+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);
+INFO: ([[[[0, 1, 2, 3, 4, 5, 6], [0, 1, 2, 3, 4, 5, 6], [0, 1, 2, 3, 4, 5, 6], [0, 1, 2, 3, 4, 5, 6], [0, 1, 2, 3, 4, 5, 6]], [[0, 1, 2, 3, 4, 5, 6], [0, 1, 2, 3, 4, 5, 6], [0, 1, 2, 3, 4, 5, 6], [0, 1, 2, 3, 4, 5, 6], [0, 1, 2, 3, 4, 5, 6]], [[0, 1, 2, 3, 4, 5, 6], [0, 1, 2, 3, 4, 5, 6], [0, 1, 2, 3, 4, 5, 6], [0, 1, 2, 3, 4, 5, 6], [0, 1, 2, 3, 4, 5, 6]]], [[[0, 1, 2, 3, 4, 5, 6], [0, 1, 2, 3, 4, 5, 6], [0, 1, 2, 3, 4, 5, 6], [0, 1, 2, 3, 4, 5, 6], [0, 1, 2, 3, 4, 5, 6]], [[0, 1, 2, 3, 4, 5, 6], [0, 1, 2, 3, 4, 5, 6], [0, 1, 2, 3, 4, 5, 6], [0, 1, 2, 3, 4, 5, 6], [0, 1, 2, 3, 4, 5, 6]], [[0, 1, 2, 3, 4, 5, 6], [0, 1, 2, 3, 4, 5, 6], [0, 1, 2, 3, 4, 5, 6], [0, 1, 2, 3, 4, 5, 6], [0, 1, 2, 3, 4, 5, 6]]]], <class 'list'>)
+ pyreturnmultidemfloat8
+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ {{{{0,1,2,3,4,5,6},{0,1,2,3,4,5,6},{0,1,2,3,4,5,6},{0,1,2,3,4,5,6},{0,1,2,3,4,5,6}},{{0,1,2,3,4,5,6},{0,1,2,3,4,5,6},{0,1,2,3,4,5,6},{0,1,2,3,4,5,6},{0,1,2,3,4,5,6}},{{0,1,2,3,4,5,6},{0,1,2,3,4,5,6},{0,1,2,3,4,5,6},{0,1,2,3,4,5,6},{0,1,2,3,4,5,6}}},{{{0,1,2,3,4,5,6},{0,1,2,3,4,5,6},{0,1,2,3,4,5,6},{0,1,2,3,4,5,6},{0,1,2,3,4,5,6}},{{0,1,2,3,4,5,6},{0,1,2,3,4,5,6},{0,1,2,3,4,5,6},{0,1,2,3,4,5,6},{0,1,2,3,4,5,6}},{{0,1,2,3,4,5,6},{0,1,2,3,4,5,6},{0,1,2,3,4,5,6},{0,1,2,3,4,5,6},{0,1,2,3,4,5,6}}}}
+(1 row)
+
+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']);
+INFO: (['foo', 'bar'], <class 'list'>)
+ test_type_conversion_array_text
+---------------------------------
+ {foo,bar}
+(1 row)
+
+SELECT * FROM test_type_conversion_array_text(ARRAY[['foo', 'bar'],['foo2', 'bar2']]);
+INFO: ([['foo', 'bar'], ['foo2', 'bar2']], <class 'list'>)
+ test_type_conversion_array_text
+---------------------------------
+ {{foo,bar},{foo2,bar2}}
+(1 row)
+
+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]);
+INFO: ([b'\xde\xad\xbe\xef', None], <class 'list'>)
+ test_type_conversion_array_bytea
+----------------------------------
+ {"\\xdeadbeef",NULL}
+(1 row)
+
+CREATE FUNCTION test_type_conversion_array_mixed1() RETURNS text[] AS $$
+return [123, 'abc']
+$$ LANGUAGE plpython3u;
+SELECT * FROM test_type_conversion_array_mixed1();
+ test_type_conversion_array_mixed1
+-----------------------------------
+ {123,abc}
+(1 row)
+
+CREATE FUNCTION test_type_conversion_array_mixed2() RETURNS int[] AS $$
+return [123, 'abc']
+$$ LANGUAGE plpython3u;
+SELECT * FROM test_type_conversion_array_mixed2();
+ERROR: invalid input syntax for type integer: "abc"
+CONTEXT: while creating return value
+PL/Python function "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();
+ test_type_conversion_md_array_out
+-----------------------------------
+ {{a},{b},{c}}
+(1 row)
+
+CREATE OR REPLACE FUNCTION test_type_conversion_md_array_out() RETURNS text[] AS $$
+return [[], []]
+$$ LANGUAGE plpython3u;
+select test_type_conversion_md_array_out();
+ test_type_conversion_md_array_out
+-----------------------------------
+ {}
+(1 row)
+
+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
+ERROR: multidimensional arrays must have array expressions with matching dimensions
+CONTEXT: while creating return value
+PL/Python function "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
+ERROR: multidimensional arrays must have array expressions with matching dimensions
+CONTEXT: while creating return value
+PL/Python function "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
+ERROR: multidimensional arrays must have array expressions with matching dimensions
+CONTEXT: while creating return value
+PL/Python function "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
+ERROR: multidimensional arrays must have array expressions with matching dimensions
+CONTEXT: while creating return value
+PL/Python function "test_type_conversion_md_array_out"
+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();
+ERROR: multidimensional arrays must have array expressions with matching dimensions
+CONTEXT: while creating return value
+PL/Python function "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();
+ERROR: multidimensional arrays must have array expressions with matching dimensions
+CONTEXT: while creating return value
+PL/Python function "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();
+ERROR: multidimensional arrays must have array expressions with matching dimensions
+CONTEXT: while creating return value
+PL/Python function "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();
+ERROR: number of array dimensions exceeds the maximum allowed (6)
+CONTEXT: while creating return value
+PL/Python function "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();
+ test_type_conversion_array_record
+-----------------------------------
+ {"(one,42)","(two,11)"}
+(1 row)
+
+CREATE FUNCTION test_type_conversion_array_string() RETURNS text[] AS $$
+return 'abc'
+$$ LANGUAGE plpython3u;
+SELECT * FROM test_type_conversion_array_string();
+ test_type_conversion_array_string
+-----------------------------------
+ {a,b,c}
+(1 row)
+
+CREATE FUNCTION test_type_conversion_array_tuple() RETURNS text[] AS $$
+return ('abc', 'def')
+$$ LANGUAGE plpython3u;
+SELECT * FROM test_type_conversion_array_tuple();
+ test_type_conversion_array_tuple
+----------------------------------
+ {abc,def}
+(1 row)
+
+CREATE FUNCTION test_type_conversion_array_error() RETURNS int[] AS $$
+return 5
+$$ LANGUAGE plpython3u;
+SELECT * FROM test_type_conversion_array_error();
+ERROR: return value of function with array return type is not a Python sequence
+CONTEXT: while creating return value
+PL/Python function "test_type_conversion_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);
+INFO: ([0, 100], <class 'list'>)
+ test_type_conversion_array_domain
+-----------------------------------
+ {0,100}
+(1 row)
+
+SELECT * FROM test_type_conversion_array_domain(NULL::ordered_pair_domain);
+INFO: (None, <class 'NoneType'>)
+ test_type_conversion_array_domain
+-----------------------------------
+
+(1 row)
+
+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();
+ERROR: value for domain ordered_pair_domain violates check constraint "ordered_pair_domain_check"
+CONTEXT: while creating return value
+PL/Python function "test_type_conversion_array_domain_check_violation"
+--
+-- Arrays of domains
+--
+CREATE FUNCTION test_read_uint2_array(x uint2[]) RETURNS uint2 AS $$
+plpy.info(x, type(x))
+return x[0]
+$$ LANGUAGE plpython3u;
+select test_read_uint2_array(array[1::uint2]);
+INFO: ([1], <class 'list'>)
+ test_read_uint2_array
+-----------------------
+ 1
+(1 row)
+
+CREATE FUNCTION test_build_uint2_array(x int2) RETURNS uint2[] AS $$
+return [x, x]
+$$ LANGUAGE plpython3u;
+select test_build_uint2_array(1::int2);
+ test_build_uint2_array
+------------------------
+ {1,1}
+(1 row)
+
+select test_build_uint2_array(-1::int2); -- fail
+ERROR: value for domain uint2 violates check constraint "uint2_check"
+CONTEXT: while creating return value
+PL/Python function "test_build_uint2_array"
+--
+-- ideally this would work, but for now it doesn't, because the return value
+-- is [[2,4], [2,4]] which our conversion code thinks should become a 2-D
+-- integer array, not an array of arrays.
+--
+CREATE FUNCTION test_type_conversion_domain_array(x integer[])
+ RETURNS ordered_pair_domain[] AS $$
+return [x, x]
+$$ LANGUAGE plpython3u;
+select test_type_conversion_domain_array(array[2,4]);
+ERROR: return value of function with array return type is not a Python sequence
+CONTEXT: while creating return value
+PL/Python function "test_type_conversion_domain_array"
+select test_type_conversion_domain_array(array[4,2]); -- fail
+ERROR: return value of function with array return type is not a Python sequence
+CONTEXT: while creating return value
+PL/Python function "test_type_conversion_domain_array"
+CREATE FUNCTION test_type_conversion_domain_array2(x ordered_pair_domain)
+ RETURNS integer AS $$
+plpy.info(x, type(x))
+return x[1]
+$$ LANGUAGE plpython3u;
+select test_type_conversion_domain_array2(array[2,4]);
+INFO: ([2, 4], <class 'list'>)
+ test_type_conversion_domain_array2
+------------------------------------
+ 4
+(1 row)
+
+select test_type_conversion_domain_array2(array[4,2]); -- fail
+ERROR: value for domain ordered_pair_domain violates check constraint "ordered_pair_domain_check"
+CREATE FUNCTION test_type_conversion_array_domain_array(x ordered_pair_domain[])
+ RETURNS ordered_pair_domain AS $$
+plpy.info(x, type(x))
+return x[0]
+$$ LANGUAGE plpython3u;
+select test_type_conversion_array_domain_array(array[array[2,4]::ordered_pair_domain]);
+INFO: ([[2, 4]], <class 'list'>)
+ test_type_conversion_array_domain_array
+-----------------------------------------
+ {2,4}
+(1 row)
+
+---
+--- Composite types
+---
+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;
+ name | test_composite_table_input
+------+----------------------------
+ John | 110
+ Mary | 210
+(2 rows)
+
+ALTER TABLE employee DROP bonus;
+SELECT name, test_composite_table_input(employee.*) FROM employee;
+ERROR: KeyError: 'bonus'
+CONTEXT: Traceback (most recent call last):
+ PL/Python function "test_composite_table_input", line 2, in <module>
+ return e['basesalary'] + e['bonus']
+PL/Python function "test_composite_table_input"
+ALTER TABLE employee ADD bonus integer;
+UPDATE employee SET bonus = 10;
+SELECT name, test_composite_table_input(employee.*) FROM employee;
+ name | test_composite_table_input
+------+----------------------------
+ John | 110
+ Mary | 210
+(2 rows)
+
+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));
+ test_composite_type_input
+---------------------------
+ 3
+(1 row)
+
+ALTER TYPE named_pair RENAME TO named_pair_2;
+SELECT test_composite_type_input(row(1, 2));
+ test_composite_type_input
+---------------------------
+ 3
+(1 row)
+
+--
+-- Domains within composite
+--
+CREATE TYPE nnint_container AS (f1 int, f2 nnint);
+CREATE FUNCTION nnint_test(x int, y int) RETURNS nnint_container AS $$
+return {'f1': x, 'f2': y}
+$$ LANGUAGE plpython3u;
+SELECT nnint_test(null, 3);
+ nnint_test
+------------
+ (,3)
+(1 row)
+
+SELECT nnint_test(3, null); -- fail
+ERROR: value for domain nnint violates check constraint "nnint_check"
+CONTEXT: while creating return value
+PL/Python function "nnint_test"
+--
+-- Domains of composite
+--
+CREATE DOMAIN ordered_named_pair AS named_pair_2 CHECK((VALUE).i <= (VALUE).j);
+CREATE FUNCTION read_ordered_named_pair(p ordered_named_pair) RETURNS integer AS $$
+return p['i'] + p['j']
+$$ LANGUAGE plpython3u;
+SELECT read_ordered_named_pair(row(1, 2));
+ read_ordered_named_pair
+-------------------------
+ 3
+(1 row)
+
+SELECT read_ordered_named_pair(row(2, 1)); -- fail
+ERROR: value for domain ordered_named_pair violates check constraint "ordered_named_pair_check"
+CREATE FUNCTION build_ordered_named_pair(i int, j int) RETURNS ordered_named_pair AS $$
+return {'i': i, 'j': j}
+$$ LANGUAGE plpython3u;
+SELECT build_ordered_named_pair(1,2);
+ build_ordered_named_pair
+--------------------------
+ (1,2)
+(1 row)
+
+SELECT build_ordered_named_pair(2,1); -- fail
+ERROR: value for domain ordered_named_pair violates check constraint "ordered_named_pair_check"
+CONTEXT: while creating return value
+PL/Python function "build_ordered_named_pair"
+CREATE FUNCTION build_ordered_named_pairs(i int, j int) RETURNS ordered_named_pair[] AS $$
+return [{'i': i, 'j': j}, {'i': i, 'j': j+1}]
+$$ LANGUAGE plpython3u;
+SELECT build_ordered_named_pairs(1,2);
+ build_ordered_named_pairs
+---------------------------
+ {"(1,2)","(1,3)"}
+(1 row)
+
+SELECT build_ordered_named_pairs(2,1); -- fail
+ERROR: value for domain ordered_named_pair violates check constraint "ordered_named_pair_check"
+CONTEXT: while creating return value
+PL/Python function "build_ordered_named_pairs"
+--
+-- Prepared statements
+--
+CREATE OR REPLACE FUNCTION test_prep_bool_input() RETURNS int
+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
+ test_prep_bool_input
+----------------------
+ 1
+(1 row)
+
+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
+INFO: {'val': False}
+ test_prep_bool_output
+-----------------------
+ f
+(1 row)
+
+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)
+ test_prep_bytea_input
+-----------------------
+ 3
+(1 row)
+
+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();
+INFO: {'val': b'\xaa\x00\xbb'}
+ test_prep_bytea_output
+------------------------
+ \xaa00bb
+(1 row)
+
diff --git a/src/pl/plpython/expected/plpython_unicode.out b/src/pl/plpython/expected/plpython_unicode.out
new file mode 100644
index 0000000..fd54b0b
--- /dev/null
+++ b/src/pl/plpython/expected/plpython_unicode.out
@@ -0,0 +1,56 @@
+--
+-- 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();
+ unicode_return
+----------------
+  
+(1 row)
+
+INSERT INTO unicode_test (testvalue) VALUES ('test');
+SELECT * FROM unicode_test;
+ testvalue
+-----------
+  
+(1 row)
+
+SELECT unicode_plan1();
+ unicode_plan1
+---------------
+  
+(1 row)
+
+SELECT unicode_plan2();
+ unicode_plan2
+---------------
+ foobar
+(1 row)
+
diff --git a/src/pl/plpython/expected/plpython_void.out b/src/pl/plpython/expected/plpython_void.out
new file mode 100644
index 0000000..07d0760
--- /dev/null
+++ b/src/pl/plpython/expected/plpython_void.out
@@ -0,0 +1,30 @@
+--
+-- 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";
+ test_void_func1 | is null
+-----------------+---------
+ | f
+(1 row)
+
+SELECT test_void_func2(); -- should fail
+ERROR: PL/Python function with return type "void" did not return None
+CONTEXT: while creating return value
+PL/Python function "test_void_func2"
+SELECT test_return_none(), test_return_none() IS NULL AS "is null";
+ test_return_none | is null
+------------------+---------
+ | t
+(1 row)
+
diff --git a/src/pl/plpython/generate-spiexceptions.pl b/src/pl/plpython/generate-spiexceptions.pl
new file mode 100644
index 0000000..61b37c3
--- /dev/null
+++ b/src/pl/plpython/generate-spiexceptions.pl
@@ -0,0 +1,44 @@
+#!/usr/bin/perl
+#
+# Generate the spiexceptions.h header from errcodes.txt
+# Copyright (c) 2000-2023, PostgreSQL Global Development Group
+
+use strict;
+use warnings;
+
+print
+ "/* autogenerated from src/backend/utils/errcodes.txt, do not edit */\n";
+print "/* there is deliberately not an #ifndef SPIEXCEPTIONS_H here */\n";
+
+open my $errcodes, '<', $ARGV[0] or die;
+
+while (<$errcodes>)
+{
+ chomp;
+
+ # Skip comments
+ next if /^#/;
+ next if /^\s*$/;
+
+ # Skip section headers
+ next if /^Section:/;
+
+ die unless /^([^\s]{5})\s+([EWS])\s+([^\s]+)(?:\s+)?([^\s]+)?/;
+
+ (my $sqlstate, my $type, my $errcode_macro, my $condition_name) =
+ ($1, $2, $3, $4);
+
+ # Skip non-errors
+ next unless $type eq 'E';
+
+ # Skip lines without PL/pgSQL condition names
+ next unless defined($condition_name);
+
+ # Change some_error_condition to SomeErrorCondition
+ $condition_name =~ s/([a-z])([^_]*)(?:_|$)/\u$1$2/g;
+
+ print "\n{\n\t\"spiexceptions.$condition_name\", "
+ . "\"$condition_name\", $errcode_macro\n},\n";
+}
+
+close $errcodes;
diff --git a/src/pl/plpython/meson.build b/src/pl/plpython/meson.build
new file mode 100644
index 0000000..98b7d7c
--- /dev/null
+++ b/src/pl/plpython/meson.build
@@ -0,0 +1,109 @@
+# Copyright (c) 2022-2023, PostgreSQL Global Development Group
+
+if not python3_dep.found()
+ subdir_done()
+endif
+
+plpython_sources = files(
+ 'plpy_cursorobject.c',
+ 'plpy_elog.c',
+ 'plpy_exec.c',
+ 'plpy_main.c',
+ 'plpy_planobject.c',
+ 'plpy_plpymodule.c',
+ 'plpy_procedure.c',
+ 'plpy_resultobject.c',
+ 'plpy_spi.c',
+ 'plpy_subxactobject.c',
+ 'plpy_typeio.c',
+ 'plpy_util.c',
+)
+
+plpython_sources += custom_target('spiexceptions.h',
+ input: files('../../backend/utils/errcodes.txt'),
+ output: 'spiexceptions.h',
+ command: [perl, files('generate-spiexceptions.pl'), '@INPUT@'],
+ capture: true,
+)
+
+
+# FIXME: need to duplicate import library ugliness?
+plpython_inc = include_directories('.')
+
+if host_system == 'windows'
+ plpython_sources += rc_lib_gen.process(win32ver_rc, extra_args: [
+ '--NAME', 'plpython3',
+ '--FILEDESC', 'PL/Python - procedural language',])
+endif
+
+plpython = shared_module('plpython3',
+ plpython_sources,
+ c_pch: pch_postgres_h,
+ include_directories: [plpython_inc, postgres_inc],
+ kwargs: pg_mod_args + {
+ 'dependencies': [python3_dep, pg_mod_args['dependencies']],
+ },
+)
+pl_targets += plpython
+
+# FIXME: Only install the relevant versions
+install_data(
+ 'plpython3u.control',
+ 'plpython3u--1.0.sql',
+ install_dir: dir_data_extension,
+)
+
+install_headers(
+ 'plpy_cursorobject.h',
+ 'plpy_elog.h',
+ 'plpy_exec.h',
+ 'plpy_main.h',
+ 'plpy_planobject.h',
+ 'plpy_plpymodule.h',
+ 'plpy_procedure.h',
+ 'plpy_resultobject.h',
+ 'plpy_spi.h',
+ 'plpy_subxactobject.h',
+ 'plpy_typeio.h',
+ 'plpy_util.h',
+ 'plpython.h',
+ 'plpython_system.h',
+ install_dir: dir_include_server,
+)
+
+plpython_regress = [
+ 'plpython_schema',
+ 'plpython_populate',
+ 'plpython_test',
+ 'plpython_do',
+ 'plpython_global',
+ 'plpython_import',
+ 'plpython_spi',
+ 'plpython_newline',
+ 'plpython_void',
+ 'plpython_call',
+ 'plpython_params',
+ 'plpython_setof',
+ 'plpython_record',
+ 'plpython_trigger',
+ 'plpython_types',
+ 'plpython_error',
+ 'plpython_ereport',
+ 'plpython_unicode',
+ 'plpython_quote',
+ 'plpython_composite',
+ 'plpython_subtransaction',
+ 'plpython_transaction',
+ 'plpython_drop',
+]
+
+tests += {
+ 'name': 'plpython',
+ 'sd': meson.current_source_dir(),
+ 'bd': meson.current_build_dir(),
+ 'regress': {
+ 'sql': plpython_regress,
+ },
+}
+
+subdir('po', if_found: libintl)
diff --git a/src/pl/plpython/nls.mk b/src/pl/plpython/nls.mk
new file mode 100644
index 0000000..e7a25ca
--- /dev/null
+++ b/src/pl/plpython/nls.mk
@@ -0,0 +1,20 @@
+# src/pl/plpython/nls.mk
+CATALOG_NAME = plpython
+GETTEXT_FILES = plpy_cursorobject.c \
+ plpy_elog.c \
+ plpy_exec.c \
+ plpy_main.c \
+ plpy_planobject.c \
+ plpy_plpymodule.c \
+ plpy_procedure.c \
+ plpy_resultobject.c \
+ plpy_spi.c \
+ plpy_subxactobject.c \
+ plpy_typeio.c \
+ plpy_util.c
+GETTEXT_TRIGGERS = $(BACKEND_COMMON_GETTEXT_TRIGGERS) PLy_elog:2 PLy_exception_set:2 PLy_exception_set_plural:2,3
+GETTEXT_FLAGS = $(BACKEND_COMMON_GETTEXT_FLAGS) \
+ PLy_elog:2:c-format \
+ PLy_exception_set:2:c-format \
+ PLy_exception_set_plural:2:c-format \
+ PLy_exception_set_plural:3:c-format
diff --git a/src/pl/plpython/plpy_cursorobject.c b/src/pl/plpython/plpy_cursorobject.c
new file mode 100644
index 0000000..57e8f8e
--- /dev/null
+++ b/src/pl/plpython/plpy_cursorobject.c
@@ -0,0 +1,492 @@
+/*
+ * the PLyCursor class
+ *
+ * src/pl/plpython/plpy_cursorobject.c
+ */
+
+#include "postgres.h"
+
+#include <limits.h>
+
+#include "access/xact.h"
+#include "catalog/pg_type.h"
+#include "mb/pg_wchar.h"
+#include "plpy_cursorobject.h"
+#include "plpy_elog.h"
+#include "plpy_main.h"
+#include "plpy_planobject.h"
+#include "plpy_procedure.h"
+#include "plpy_resultobject.h"
+#include "plpy_spi.h"
+#include "plpython.h"
+#include "utils/memutils.h"
+
+static PyObject *PLy_cursor_query(const char *query);
+static void PLy_cursor_dealloc(PyObject *arg);
+static PyObject *PLy_cursor_iternext(PyObject *self);
+static PyObject *PLy_cursor_fetch(PyObject *self, PyObject *args);
+static PyObject *PLy_cursor_close(PyObject *self, PyObject *unused);
+
+static char PLy_cursor_doc[] = "Wrapper around a PostgreSQL cursor";
+
+static PyMethodDef PLy_cursor_methods[] = {
+ {"fetch", PLy_cursor_fetch, METH_VARARGS, NULL},
+ {"close", PLy_cursor_close, METH_NOARGS, NULL},
+ {NULL, NULL, 0, NULL}
+};
+
+static PyTypeObject PLy_CursorType = {
+ PyVarObject_HEAD_INIT(NULL, 0)
+ .tp_name = "PLyCursor",
+ .tp_basicsize = sizeof(PLyCursorObject),
+ .tp_dealloc = PLy_cursor_dealloc,
+ .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,
+ .tp_doc = PLy_cursor_doc,
+ .tp_iter = PyObject_SelfIter,
+ .tp_iternext = PLy_cursor_iternext,
+ .tp_methods = PLy_cursor_methods,
+};
+
+void
+PLy_cursor_init_type(void)
+{
+ if (PyType_Ready(&PLy_CursorType) < 0)
+ elog(ERROR, "could not initialize PLy_CursorType");
+}
+
+PyObject *
+PLy_cursor(PyObject *self, PyObject *args)
+{
+ char *query;
+ PyObject *plan;
+ PyObject *planargs = NULL;
+
+ if (PyArg_ParseTuple(args, "s", &query))
+ return PLy_cursor_query(query);
+
+ PyErr_Clear();
+
+ if (PyArg_ParseTuple(args, "O|O", &plan, &planargs))
+ return PLy_cursor_plan(plan, planargs);
+
+ PLy_exception_set(PLy_exc_error, "plpy.cursor expected a query or a plan");
+ return NULL;
+}
+
+
+static PyObject *
+PLy_cursor_query(const char *query)
+{
+ PLyCursorObject *cursor;
+ PLyExecutionContext *exec_ctx = PLy_current_execution_context();
+ volatile MemoryContext oldcontext;
+ volatile ResourceOwner oldowner;
+
+ if ((cursor = PyObject_New(PLyCursorObject, &PLy_CursorType)) == NULL)
+ return NULL;
+ cursor->portalname = NULL;
+ cursor->closed = false;
+ cursor->mcxt = AllocSetContextCreate(TopMemoryContext,
+ "PL/Python cursor context",
+ ALLOCSET_DEFAULT_SIZES);
+
+ /* Initialize for converting result tuples to Python */
+ PLy_input_setup_func(&cursor->result, cursor->mcxt,
+ RECORDOID, -1,
+ exec_ctx->curr_proc);
+
+ oldcontext = CurrentMemoryContext;
+ oldowner = CurrentResourceOwner;
+
+ PLy_spi_subtransaction_begin(oldcontext, oldowner);
+
+ PG_TRY();
+ {
+ SPIPlanPtr plan;
+ Portal portal;
+
+ pg_verifymbstr(query, strlen(query), false);
+
+ plan = SPI_prepare(query, 0, NULL);
+ if (plan == NULL)
+ elog(ERROR, "SPI_prepare failed: %s",
+ SPI_result_code_string(SPI_result));
+
+ portal = SPI_cursor_open(NULL, plan, NULL, NULL,
+ exec_ctx->curr_proc->fn_readonly);
+ SPI_freeplan(plan);
+
+ if (portal == NULL)
+ elog(ERROR, "SPI_cursor_open() failed: %s",
+ SPI_result_code_string(SPI_result));
+
+ cursor->portalname = MemoryContextStrdup(cursor->mcxt, portal->name);
+
+ PinPortal(portal);
+
+ PLy_spi_subtransaction_commit(oldcontext, oldowner);
+ }
+ PG_CATCH();
+ {
+ PLy_spi_subtransaction_abort(oldcontext, oldowner);
+ return NULL;
+ }
+ PG_END_TRY();
+
+ Assert(cursor->portalname != NULL);
+ return (PyObject *) cursor;
+}
+
+PyObject *
+PLy_cursor_plan(PyObject *ob, PyObject *args)
+{
+ PLyCursorObject *cursor;
+ volatile int nargs;
+ int i;
+ PLyPlanObject *plan;
+ PLyExecutionContext *exec_ctx = PLy_current_execution_context();
+ volatile MemoryContext oldcontext;
+ volatile ResourceOwner oldowner;
+
+ if (args)
+ {
+ if (!PySequence_Check(args) || PyUnicode_Check(args))
+ {
+ PLy_exception_set(PyExc_TypeError, "plpy.cursor takes a sequence as its second argument");
+ return NULL;
+ }
+ nargs = PySequence_Length(args);
+ }
+ else
+ nargs = 0;
+
+ plan = (PLyPlanObject *) ob;
+
+ if (nargs != plan->nargs)
+ {
+ char *sv;
+ PyObject *so = PyObject_Str(args);
+
+ if (!so)
+ PLy_elog(ERROR, "could not execute plan");
+ sv = PLyUnicode_AsString(so);
+ PLy_exception_set_plural(PyExc_TypeError,
+ "Expected sequence of %d argument, got %d: %s",
+ "Expected sequence of %d arguments, got %d: %s",
+ plan->nargs,
+ plan->nargs, nargs, sv);
+ Py_DECREF(so);
+
+ return NULL;
+ }
+
+ if ((cursor = PyObject_New(PLyCursorObject, &PLy_CursorType)) == NULL)
+ return NULL;
+ cursor->portalname = NULL;
+ cursor->closed = false;
+ cursor->mcxt = AllocSetContextCreate(TopMemoryContext,
+ "PL/Python cursor context",
+ ALLOCSET_DEFAULT_SIZES);
+
+ /* Initialize for converting result tuples to Python */
+ PLy_input_setup_func(&cursor->result, cursor->mcxt,
+ RECORDOID, -1,
+ exec_ctx->curr_proc);
+
+ oldcontext = CurrentMemoryContext;
+ oldowner = CurrentResourceOwner;
+
+ PLy_spi_subtransaction_begin(oldcontext, oldowner);
+
+ PG_TRY();
+ {
+ Portal portal;
+ char *volatile nulls;
+ volatile int j;
+
+ if (nargs > 0)
+ nulls = palloc(nargs * sizeof(char));
+ else
+ nulls = NULL;
+
+ for (j = 0; j < nargs; j++)
+ {
+ PLyObToDatum *arg = &plan->args[j];
+ PyObject *elem;
+
+ elem = PySequence_GetItem(args, j);
+ PG_TRY(2);
+ {
+ bool isnull;
+
+ plan->values[j] = PLy_output_convert(arg, elem, &isnull);
+ nulls[j] = isnull ? 'n' : ' ';
+ }
+ PG_FINALLY(2);
+ {
+ Py_DECREF(elem);
+ }
+ PG_END_TRY(2);
+ }
+
+ portal = SPI_cursor_open(NULL, plan->plan, plan->values, nulls,
+ exec_ctx->curr_proc->fn_readonly);
+ if (portal == NULL)
+ elog(ERROR, "SPI_cursor_open() failed: %s",
+ SPI_result_code_string(SPI_result));
+
+ cursor->portalname = MemoryContextStrdup(cursor->mcxt, portal->name);
+
+ PinPortal(portal);
+
+ PLy_spi_subtransaction_commit(oldcontext, oldowner);
+ }
+ PG_CATCH();
+ {
+ int k;
+
+ /* cleanup plan->values array */
+ for (k = 0; k < nargs; k++)
+ {
+ if (!plan->args[k].typbyval &&
+ (plan->values[k] != PointerGetDatum(NULL)))
+ {
+ pfree(DatumGetPointer(plan->values[k]));
+ plan->values[k] = PointerGetDatum(NULL);
+ }
+ }
+
+ Py_DECREF(cursor);
+
+ PLy_spi_subtransaction_abort(oldcontext, oldowner);
+ return NULL;
+ }
+ PG_END_TRY();
+
+ for (i = 0; i < nargs; i++)
+ {
+ if (!plan->args[i].typbyval &&
+ (plan->values[i] != PointerGetDatum(NULL)))
+ {
+ pfree(DatumGetPointer(plan->values[i]));
+ plan->values[i] = PointerGetDatum(NULL);
+ }
+ }
+
+ Assert(cursor->portalname != NULL);
+ return (PyObject *) cursor;
+}
+
+static void
+PLy_cursor_dealloc(PyObject *arg)
+{
+ PLyCursorObject *cursor;
+ Portal portal;
+
+ cursor = (PLyCursorObject *) arg;
+
+ if (!cursor->closed)
+ {
+ portal = GetPortalByName(cursor->portalname);
+
+ if (PortalIsValid(portal))
+ {
+ UnpinPortal(portal);
+ SPI_cursor_close(portal);
+ }
+ cursor->closed = true;
+ }
+ if (cursor->mcxt)
+ {
+ MemoryContextDelete(cursor->mcxt);
+ cursor->mcxt = NULL;
+ }
+ arg->ob_type->tp_free(arg);
+}
+
+static PyObject *
+PLy_cursor_iternext(PyObject *self)
+{
+ PLyCursorObject *cursor;
+ PyObject *ret;
+ PLyExecutionContext *exec_ctx = PLy_current_execution_context();
+ volatile MemoryContext oldcontext;
+ volatile ResourceOwner oldowner;
+ Portal portal;
+
+ cursor = (PLyCursorObject *) self;
+
+ if (cursor->closed)
+ {
+ PLy_exception_set(PyExc_ValueError, "iterating a closed cursor");
+ return NULL;
+ }
+
+ portal = GetPortalByName(cursor->portalname);
+ if (!PortalIsValid(portal))
+ {
+ PLy_exception_set(PyExc_ValueError,
+ "iterating a cursor in an aborted subtransaction");
+ return NULL;
+ }
+
+ oldcontext = CurrentMemoryContext;
+ oldowner = CurrentResourceOwner;
+
+ PLy_spi_subtransaction_begin(oldcontext, oldowner);
+
+ PG_TRY();
+ {
+ SPI_cursor_fetch(portal, true, 1);
+ if (SPI_processed == 0)
+ {
+ PyErr_SetNone(PyExc_StopIteration);
+ ret = NULL;
+ }
+ else
+ {
+ PLy_input_setup_tuple(&cursor->result, SPI_tuptable->tupdesc,
+ exec_ctx->curr_proc);
+
+ ret = PLy_input_from_tuple(&cursor->result, SPI_tuptable->vals[0],
+ SPI_tuptable->tupdesc, true);
+ }
+
+ SPI_freetuptable(SPI_tuptable);
+
+ PLy_spi_subtransaction_commit(oldcontext, oldowner);
+ }
+ PG_CATCH();
+ {
+ PLy_spi_subtransaction_abort(oldcontext, oldowner);
+ return NULL;
+ }
+ PG_END_TRY();
+
+ return ret;
+}
+
+static PyObject *
+PLy_cursor_fetch(PyObject *self, PyObject *args)
+{
+ PLyCursorObject *cursor;
+ int count;
+ PLyResultObject *ret;
+ PLyExecutionContext *exec_ctx = PLy_current_execution_context();
+ volatile MemoryContext oldcontext;
+ volatile ResourceOwner oldowner;
+ Portal portal;
+
+ if (!PyArg_ParseTuple(args, "i:fetch", &count))
+ return NULL;
+
+ cursor = (PLyCursorObject *) self;
+
+ if (cursor->closed)
+ {
+ PLy_exception_set(PyExc_ValueError, "fetch from a closed cursor");
+ return NULL;
+ }
+
+ portal = GetPortalByName(cursor->portalname);
+ if (!PortalIsValid(portal))
+ {
+ PLy_exception_set(PyExc_ValueError,
+ "iterating a cursor in an aborted subtransaction");
+ return NULL;
+ }
+
+ ret = (PLyResultObject *) PLy_result_new();
+ if (ret == NULL)
+ return NULL;
+
+ oldcontext = CurrentMemoryContext;
+ oldowner = CurrentResourceOwner;
+
+ PLy_spi_subtransaction_begin(oldcontext, oldowner);
+
+ PG_TRY();
+ {
+ SPI_cursor_fetch(portal, true, count);
+
+ Py_DECREF(ret->status);
+ ret->status = PyLong_FromLong(SPI_OK_FETCH);
+
+ Py_DECREF(ret->nrows);
+ ret->nrows = PyLong_FromUnsignedLongLong(SPI_processed);
+
+ if (SPI_processed != 0)
+ {
+ uint64 i;
+
+ /*
+ * PyList_New() and PyList_SetItem() use Py_ssize_t for list size
+ * and list indices; so we cannot support a result larger than
+ * PY_SSIZE_T_MAX.
+ */
+ if (SPI_processed > (uint64) PY_SSIZE_T_MAX)
+ ereport(ERROR,
+ (errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED),
+ errmsg("query result has too many rows to fit in a Python list")));
+
+ Py_DECREF(ret->rows);
+ ret->rows = PyList_New(SPI_processed);
+ if (!ret->rows)
+ {
+ Py_DECREF(ret);
+ ret = NULL;
+ }
+ else
+ {
+ PLy_input_setup_tuple(&cursor->result, SPI_tuptable->tupdesc,
+ exec_ctx->curr_proc);
+
+ for (i = 0; i < SPI_processed; i++)
+ {
+ PyObject *row = PLy_input_from_tuple(&cursor->result,
+ SPI_tuptable->vals[i],
+ SPI_tuptable->tupdesc,
+ true);
+
+ PyList_SetItem(ret->rows, i, row);
+ }
+ }
+ }
+
+ SPI_freetuptable(SPI_tuptable);
+
+ PLy_spi_subtransaction_commit(oldcontext, oldowner);
+ }
+ PG_CATCH();
+ {
+ PLy_spi_subtransaction_abort(oldcontext, oldowner);
+ return NULL;
+ }
+ PG_END_TRY();
+
+ return (PyObject *) ret;
+}
+
+static PyObject *
+PLy_cursor_close(PyObject *self, PyObject *unused)
+{
+ PLyCursorObject *cursor = (PLyCursorObject *) self;
+
+ if (!cursor->closed)
+ {
+ Portal portal = GetPortalByName(cursor->portalname);
+
+ if (!PortalIsValid(portal))
+ {
+ PLy_exception_set(PyExc_ValueError,
+ "closing a cursor in an aborted subtransaction");
+ return NULL;
+ }
+
+ UnpinPortal(portal);
+ SPI_cursor_close(portal);
+ cursor->closed = true;
+ }
+
+ Py_RETURN_NONE;
+}
diff --git a/src/pl/plpython/plpy_cursorobject.h b/src/pl/plpython/plpy_cursorobject.h
new file mode 100644
index 0000000..e4d2c0e
--- /dev/null
+++ b/src/pl/plpython/plpy_cursorobject.h
@@ -0,0 +1,24 @@
+/*
+ * src/pl/plpython/plpy_cursorobject.h
+ */
+
+#ifndef PLPY_CURSOROBJECT_H
+#define PLPY_CURSOROBJECT_H
+
+#include "plpy_typeio.h"
+
+
+typedef struct PLyCursorObject
+{
+ PyObject_HEAD
+ char *portalname;
+ PLyDatumToOb result;
+ bool closed;
+ MemoryContext mcxt;
+} PLyCursorObject;
+
+extern void PLy_cursor_init_type(void);
+extern PyObject *PLy_cursor(PyObject *self, PyObject *args);
+extern PyObject *PLy_cursor_plan(PyObject *ob, PyObject *args);
+
+#endif /* PLPY_CURSOROBJECT_H */
diff --git a/src/pl/plpython/plpy_elog.c b/src/pl/plpython/plpy_elog.c
new file mode 100644
index 0000000..70de5ba
--- /dev/null
+++ b/src/pl/plpython/plpy_elog.c
@@ -0,0 +1,599 @@
+/*
+ * reporting Python exceptions as PostgreSQL errors
+ *
+ * src/pl/plpython/plpy_elog.c
+ */
+
+#include "postgres.h"
+
+#include "lib/stringinfo.h"
+#include "plpy_elog.h"
+#include "plpy_main.h"
+#include "plpy_procedure.h"
+#include "plpython.h"
+
+PyObject *PLy_exc_error = NULL;
+PyObject *PLy_exc_fatal = NULL;
+PyObject *PLy_exc_spi_error = NULL;
+
+
+static void PLy_traceback(PyObject *e, PyObject *v, PyObject *tb,
+ char **xmsg, char **tbmsg, int *tb_depth);
+static void PLy_get_spi_error_data(PyObject *exc, int *sqlerrcode, char **detail,
+ char **hint, char **query, int *position,
+ char **schema_name, char **table_name, char **column_name,
+ char **datatype_name, char **constraint_name);
+static void PLy_get_error_data(PyObject *exc, int *sqlerrcode, char **detail,
+ char **hint, char **schema_name, char **table_name, char **column_name,
+ char **datatype_name, char **constraint_name);
+static char *get_source_line(const char *src, int lineno);
+
+static void get_string_attr(PyObject *obj, char *attrname, char **str);
+static bool set_string_attr(PyObject *obj, char *attrname, char *str);
+
+/*
+ * Emit a PG error or notice, together with any available info about
+ * the current Python error, previously set by PLy_exception_set().
+ * This should be used to propagate Python errors into PG. If fmt is
+ * NULL, the Python error becomes the primary error message, otherwise
+ * it becomes the detail. If there is a Python traceback, it is put
+ * in the context.
+ */
+void
+PLy_elog_impl(int elevel, const char *fmt,...)
+{
+ int save_errno = errno;
+ char *xmsg;
+ char *tbmsg;
+ int tb_depth;
+ StringInfoData emsg;
+ PyObject *exc,
+ *val,
+ *tb;
+ const char *primary = NULL;
+ int sqlerrcode = 0;
+ char *detail = NULL;
+ char *hint = NULL;
+ char *query = NULL;
+ int position = 0;
+ char *schema_name = NULL;
+ char *table_name = NULL;
+ char *column_name = NULL;
+ char *datatype_name = NULL;
+ char *constraint_name = NULL;
+
+ PyErr_Fetch(&exc, &val, &tb);
+
+ if (exc != NULL)
+ {
+ PyErr_NormalizeException(&exc, &val, &tb);
+
+ if (PyErr_GivenExceptionMatches(val, PLy_exc_spi_error))
+ PLy_get_spi_error_data(val, &sqlerrcode,
+ &detail, &hint, &query, &position,
+ &schema_name, &table_name, &column_name,
+ &datatype_name, &constraint_name);
+ else if (PyErr_GivenExceptionMatches(val, PLy_exc_error))
+ PLy_get_error_data(val, &sqlerrcode, &detail, &hint,
+ &schema_name, &table_name, &column_name,
+ &datatype_name, &constraint_name);
+ else if (PyErr_GivenExceptionMatches(val, PLy_exc_fatal))
+ elevel = FATAL;
+ }
+
+ /* this releases our refcount on tb! */
+ PLy_traceback(exc, val, tb,
+ &xmsg, &tbmsg, &tb_depth);
+
+ if (fmt)
+ {
+ initStringInfo(&emsg);
+ for (;;)
+ {
+ va_list ap;
+ int needed;
+
+ errno = save_errno;
+ va_start(ap, fmt);
+ needed = appendStringInfoVA(&emsg, dgettext(TEXTDOMAIN, fmt), ap);
+ va_end(ap);
+ if (needed == 0)
+ break;
+ enlargeStringInfo(&emsg, needed);
+ }
+ primary = emsg.data;
+
+ /* If there's an exception message, it goes in the detail. */
+ if (xmsg)
+ detail = xmsg;
+ }
+ else
+ {
+ if (xmsg)
+ primary = xmsg;
+ }
+
+ PG_TRY();
+ {
+ ereport(elevel,
+ (errcode(sqlerrcode ? sqlerrcode : ERRCODE_EXTERNAL_ROUTINE_EXCEPTION),
+ errmsg_internal("%s", primary ? primary : "no exception data"),
+ (detail) ? errdetail_internal("%s", detail) : 0,
+ (tb_depth > 0 && tbmsg) ? errcontext("%s", tbmsg) : 0,
+ (hint) ? errhint("%s", hint) : 0,
+ (query) ? internalerrquery(query) : 0,
+ (position) ? internalerrposition(position) : 0,
+ (schema_name) ? err_generic_string(PG_DIAG_SCHEMA_NAME,
+ schema_name) : 0,
+ (table_name) ? err_generic_string(PG_DIAG_TABLE_NAME,
+ table_name) : 0,
+ (column_name) ? err_generic_string(PG_DIAG_COLUMN_NAME,
+ column_name) : 0,
+ (datatype_name) ? err_generic_string(PG_DIAG_DATATYPE_NAME,
+ datatype_name) : 0,
+ (constraint_name) ? err_generic_string(PG_DIAG_CONSTRAINT_NAME,
+ constraint_name) : 0));
+ }
+ PG_FINALLY();
+ {
+ if (fmt)
+ pfree(emsg.data);
+ if (xmsg)
+ pfree(xmsg);
+ if (tbmsg)
+ pfree(tbmsg);
+ Py_XDECREF(exc);
+ Py_XDECREF(val);
+ }
+ PG_END_TRY();
+}
+
+/*
+ * Extract a Python traceback from the given exception data.
+ *
+ * The exception error message is returned in xmsg, the traceback in
+ * tbmsg (both as palloc'd strings) and the traceback depth in
+ * tb_depth.
+ *
+ * We release refcounts on all the Python objects in the traceback stack,
+ * but not on e or v.
+ */
+static void
+PLy_traceback(PyObject *e, PyObject *v, PyObject *tb,
+ char **xmsg, char **tbmsg, int *tb_depth)
+{
+ PyObject *e_type_o;
+ PyObject *e_module_o;
+ char *e_type_s = NULL;
+ char *e_module_s = NULL;
+ PyObject *vob = NULL;
+ char *vstr;
+ StringInfoData xstr;
+ StringInfoData tbstr;
+
+ /*
+ * if no exception, return nulls
+ */
+ if (e == NULL)
+ {
+ *xmsg = NULL;
+ *tbmsg = NULL;
+ *tb_depth = 0;
+
+ return;
+ }
+
+ /*
+ * Format the exception and its value and put it in xmsg.
+ */
+
+ e_type_o = PyObject_GetAttrString(e, "__name__");
+ e_module_o = PyObject_GetAttrString(e, "__module__");
+ if (e_type_o)
+ e_type_s = PLyUnicode_AsString(e_type_o);
+ if (e_type_s)
+ e_module_s = PLyUnicode_AsString(e_module_o);
+
+ if (v && ((vob = PyObject_Str(v)) != NULL))
+ vstr = PLyUnicode_AsString(vob);
+ else
+ vstr = "unknown";
+
+ initStringInfo(&xstr);
+ if (!e_type_s || !e_module_s)
+ {
+ /* shouldn't happen */
+ appendStringInfoString(&xstr, "unrecognized exception");
+ }
+ /* mimics behavior of traceback.format_exception_only */
+ else if (strcmp(e_module_s, "builtins") == 0
+ || strcmp(e_module_s, "__main__") == 0
+ || strcmp(e_module_s, "exceptions") == 0)
+ appendStringInfoString(&xstr, e_type_s);
+ else
+ appendStringInfo(&xstr, "%s.%s", e_module_s, e_type_s);
+ appendStringInfo(&xstr, ": %s", vstr);
+
+ *xmsg = xstr.data;
+
+ /*
+ * Now format the traceback and put it in tbmsg.
+ */
+
+ *tb_depth = 0;
+ initStringInfo(&tbstr);
+ /* Mimic Python traceback reporting as close as possible. */
+ appendStringInfoString(&tbstr, "Traceback (most recent call last):");
+ while (tb != NULL && tb != Py_None)
+ {
+ PyObject *volatile tb_prev = NULL;
+ PyObject *volatile frame = NULL;
+ PyObject *volatile code = NULL;
+ PyObject *volatile name = NULL;
+ PyObject *volatile lineno = NULL;
+ PyObject *volatile filename = NULL;
+
+ PG_TRY();
+ {
+ lineno = PyObject_GetAttrString(tb, "tb_lineno");
+ if (lineno == NULL)
+ elog(ERROR, "could not get line number from Python traceback");
+
+ frame = PyObject_GetAttrString(tb, "tb_frame");
+ if (frame == NULL)
+ elog(ERROR, "could not get frame from Python traceback");
+
+ code = PyObject_GetAttrString(frame, "f_code");
+ if (code == NULL)
+ elog(ERROR, "could not get code object from Python frame");
+
+ name = PyObject_GetAttrString(code, "co_name");
+ if (name == NULL)
+ elog(ERROR, "could not get function name from Python code object");
+
+ filename = PyObject_GetAttrString(code, "co_filename");
+ if (filename == NULL)
+ elog(ERROR, "could not get file name from Python code object");
+ }
+ PG_CATCH();
+ {
+ Py_XDECREF(frame);
+ Py_XDECREF(code);
+ Py_XDECREF(name);
+ Py_XDECREF(lineno);
+ Py_XDECREF(filename);
+ PG_RE_THROW();
+ }
+ PG_END_TRY();
+
+ /* The first frame always points at <module>, skip it. */
+ if (*tb_depth > 0)
+ {
+ PLyExecutionContext *exec_ctx = PLy_current_execution_context();
+ char *proname;
+ char *fname;
+ char *line;
+ char *plain_filename;
+ long plain_lineno;
+
+ /*
+ * The second frame points at the internal function, but to mimic
+ * Python error reporting we want to say <module>.
+ */
+ if (*tb_depth == 1)
+ fname = "<module>";
+ else
+ fname = PLyUnicode_AsString(name);
+
+ proname = PLy_procedure_name(exec_ctx->curr_proc);
+ plain_filename = PLyUnicode_AsString(filename);
+ plain_lineno = PyLong_AsLong(lineno);
+
+ if (proname == NULL)
+ appendStringInfo(&tbstr, "\n PL/Python anonymous code block, line %ld, in %s",
+ plain_lineno - 1, fname);
+ else
+ appendStringInfo(&tbstr, "\n PL/Python function \"%s\", line %ld, in %s",
+ proname, plain_lineno - 1, fname);
+
+ /*
+ * function code object was compiled with "<string>" as the
+ * filename
+ */
+ if (exec_ctx->curr_proc && plain_filename != NULL &&
+ strcmp(plain_filename, "<string>") == 0)
+ {
+ /*
+ * If we know the current procedure, append the exact line
+ * from the source, again mimicking Python's traceback.py
+ * module behavior. We could store the already line-split
+ * source to avoid splitting it every time, but producing a
+ * traceback is not the most important scenario to optimize
+ * for. But we do not go as far as traceback.py in reading
+ * the source of imported modules.
+ */
+ line = get_source_line(exec_ctx->curr_proc->src, plain_lineno);
+ if (line)
+ {
+ appendStringInfo(&tbstr, "\n %s", line);
+ pfree(line);
+ }
+ }
+ }
+
+ Py_DECREF(frame);
+ Py_DECREF(code);
+ Py_DECREF(name);
+ Py_DECREF(lineno);
+ Py_DECREF(filename);
+
+ /* Release the current frame and go to the next one. */
+ tb_prev = tb;
+ tb = PyObject_GetAttrString(tb, "tb_next");
+ Assert(tb_prev != Py_None);
+ Py_DECREF(tb_prev);
+ if (tb == NULL)
+ elog(ERROR, "could not traverse Python traceback");
+ (*tb_depth)++;
+ }
+
+ /* Return the traceback. */
+ *tbmsg = tbstr.data;
+
+ Py_XDECREF(e_type_o);
+ Py_XDECREF(e_module_o);
+ Py_XDECREF(vob);
+}
+
+/*
+ * Extract error code from SPIError's sqlstate attribute.
+ */
+static void
+PLy_get_sqlerrcode(PyObject *exc, int *sqlerrcode)
+{
+ PyObject *sqlstate;
+ char *buffer;
+
+ sqlstate = PyObject_GetAttrString(exc, "sqlstate");
+ if (sqlstate == NULL)
+ return;
+
+ buffer = PLyUnicode_AsString(sqlstate);
+ if (strlen(buffer) == 5 &&
+ strspn(buffer, "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ") == 5)
+ {
+ *sqlerrcode = MAKE_SQLSTATE(buffer[0], buffer[1], buffer[2],
+ buffer[3], buffer[4]);
+ }
+
+ Py_DECREF(sqlstate);
+}
+
+/*
+ * Extract the error data from a SPIError
+ */
+static void
+PLy_get_spi_error_data(PyObject *exc, int *sqlerrcode, char **detail,
+ char **hint, char **query, int *position,
+ char **schema_name, char **table_name,
+ char **column_name,
+ char **datatype_name, char **constraint_name)
+{
+ PyObject *spidata;
+
+ spidata = PyObject_GetAttrString(exc, "spidata");
+
+ if (spidata != NULL)
+ {
+ PyArg_ParseTuple(spidata, "izzzizzzzz",
+ sqlerrcode, detail, hint, query, position,
+ schema_name, table_name, column_name,
+ datatype_name, constraint_name);
+ }
+ else
+ {
+ /*
+ * If there's no spidata, at least set the sqlerrcode. This can happen
+ * if someone explicitly raises a SPI exception from Python code.
+ */
+ PLy_get_sqlerrcode(exc, sqlerrcode);
+ }
+
+ Py_XDECREF(spidata);
+}
+
+/*
+ * Extract the error data from an Error.
+ *
+ * Note: position and query attributes are never set for Error so, unlike
+ * PLy_get_spi_error_data, this function doesn't return them.
+ */
+static void
+PLy_get_error_data(PyObject *exc, int *sqlerrcode, char **detail, char **hint,
+ char **schema_name, char **table_name, char **column_name,
+ char **datatype_name, char **constraint_name)
+{
+ PLy_get_sqlerrcode(exc, sqlerrcode);
+ get_string_attr(exc, "detail", detail);
+ get_string_attr(exc, "hint", hint);
+ get_string_attr(exc, "schema_name", schema_name);
+ get_string_attr(exc, "table_name", table_name);
+ get_string_attr(exc, "column_name", column_name);
+ get_string_attr(exc, "datatype_name", datatype_name);
+ get_string_attr(exc, "constraint_name", constraint_name);
+}
+
+/*
+ * Get the given source line as a palloc'd string
+ */
+static char *
+get_source_line(const char *src, int lineno)
+{
+ const char *s = NULL;
+ const char *next = src;
+ int current = 0;
+
+ /* sanity check */
+ if (lineno <= 0)
+ return NULL;
+
+ while (current < lineno)
+ {
+ s = next;
+ next = strchr(s + 1, '\n');
+ current++;
+ if (next == NULL)
+ break;
+ }
+
+ if (current != lineno)
+ return NULL;
+
+ while (*s && isspace((unsigned char) *s))
+ s++;
+
+ if (next == NULL)
+ return pstrdup(s);
+
+ /*
+ * Sanity check, next < s if the line was all-whitespace, which should
+ * never happen if Python reported a frame created on that line, but check
+ * anyway.
+ */
+ if (next < s)
+ return NULL;
+
+ return pnstrdup(s, next - s);
+}
+
+
+/* call PyErr_SetString with a vprint interface and translation support */
+void
+PLy_exception_set(PyObject *exc, const char *fmt,...)
+{
+ char buf[1024];
+ va_list ap;
+
+ va_start(ap, fmt);
+ vsnprintf(buf, sizeof(buf), dgettext(TEXTDOMAIN, fmt), ap);
+ va_end(ap);
+
+ PyErr_SetString(exc, buf);
+}
+
+/* same, with pluralized message */
+void
+PLy_exception_set_plural(PyObject *exc,
+ const char *fmt_singular, const char *fmt_plural,
+ unsigned long n,...)
+{
+ char buf[1024];
+ va_list ap;
+
+ va_start(ap, n);
+ vsnprintf(buf, sizeof(buf),
+ dngettext(TEXTDOMAIN, fmt_singular, fmt_plural, n),
+ ap);
+ va_end(ap);
+
+ PyErr_SetString(exc, buf);
+}
+
+/* set attributes of the given exception to details from ErrorData */
+void
+PLy_exception_set_with_details(PyObject *excclass, ErrorData *edata)
+{
+ PyObject *args = NULL;
+ PyObject *error = NULL;
+
+ args = Py_BuildValue("(s)", edata->message);
+ if (!args)
+ goto failure;
+
+ /* create a new exception with the error message as the parameter */
+ error = PyObject_CallObject(excclass, args);
+ if (!error)
+ goto failure;
+
+ if (!set_string_attr(error, "sqlstate",
+ unpack_sql_state(edata->sqlerrcode)))
+ goto failure;
+
+ if (!set_string_attr(error, "detail", edata->detail))
+ goto failure;
+
+ if (!set_string_attr(error, "hint", edata->hint))
+ goto failure;
+
+ if (!set_string_attr(error, "query", edata->internalquery))
+ goto failure;
+
+ if (!set_string_attr(error, "schema_name", edata->schema_name))
+ goto failure;
+
+ if (!set_string_attr(error, "table_name", edata->table_name))
+ goto failure;
+
+ if (!set_string_attr(error, "column_name", edata->column_name))
+ goto failure;
+
+ if (!set_string_attr(error, "datatype_name", edata->datatype_name))
+ goto failure;
+
+ if (!set_string_attr(error, "constraint_name", edata->constraint_name))
+ goto failure;
+
+ PyErr_SetObject(excclass, error);
+
+ Py_DECREF(args);
+ Py_DECREF(error);
+
+ return;
+
+failure:
+ Py_XDECREF(args);
+ Py_XDECREF(error);
+
+ elog(ERROR, "could not convert error to Python exception");
+}
+
+/* get string value of an object attribute */
+static void
+get_string_attr(PyObject *obj, char *attrname, char **str)
+{
+ PyObject *val;
+
+ val = PyObject_GetAttrString(obj, attrname);
+ if (val != NULL && val != Py_None)
+ {
+ *str = pstrdup(PLyUnicode_AsString(val));
+ }
+ Py_XDECREF(val);
+}
+
+/* set an object attribute to a string value, returns true when the set was
+ * successful
+ */
+static bool
+set_string_attr(PyObject *obj, char *attrname, char *str)
+{
+ int result;
+ PyObject *val;
+
+ if (str != NULL)
+ {
+ val = PLyUnicode_FromString(str);
+ if (!val)
+ return false;
+ }
+ else
+ {
+ val = Py_None;
+ Py_INCREF(Py_None);
+ }
+
+ result = PyObject_SetAttrString(obj, attrname, val);
+ Py_DECREF(val);
+
+ return result != -1;
+}
diff --git a/src/pl/plpython/plpy_elog.h b/src/pl/plpython/plpy_elog.h
new file mode 100644
index 0000000..dc65f2f
--- /dev/null
+++ b/src/pl/plpython/plpy_elog.h
@@ -0,0 +1,46 @@
+/*
+ * src/pl/plpython/plpy_elog.h
+ */
+
+#ifndef PLPY_ELOG_H
+#define PLPY_ELOG_H
+
+#include "plpython.h"
+
+/* global exception classes */
+extern PyObject *PLy_exc_error;
+extern PyObject *PLy_exc_fatal;
+extern PyObject *PLy_exc_spi_error;
+
+/*
+ * PLy_elog()
+ *
+ * See comments at elog() about the compiler hinting.
+ */
+#ifdef HAVE__BUILTIN_CONSTANT_P
+#define PLy_elog(elevel, ...) \
+ do { \
+ PLy_elog_impl(elevel, __VA_ARGS__); \
+ if (__builtin_constant_p(elevel) && (elevel) >= ERROR) \
+ pg_unreachable(); \
+ } while(0)
+#else /* !HAVE__BUILTIN_CONSTANT_P */
+#define PLy_elog(elevel, ...) \
+ do { \
+ const int elevel_ = (elevel); \
+ PLy_elog_impl(elevel_, __VA_ARGS__); \
+ if (elevel_ >= ERROR) \
+ pg_unreachable(); \
+ } while(0)
+#endif /* HAVE__BUILTIN_CONSTANT_P */
+
+extern PGDLLEXPORT void PLy_elog_impl(int elevel, const char *fmt,...) pg_attribute_printf(2, 3);
+
+extern PGDLLEXPORT void PLy_exception_set(PyObject *exc, const char *fmt,...) pg_attribute_printf(2, 3);
+
+extern PGDLLEXPORT void PLy_exception_set_plural(PyObject *exc, const char *fmt_singular, const char *fmt_plural,
+ unsigned long n,...) pg_attribute_printf(2, 5) pg_attribute_printf(3, 5);
+
+extern PGDLLEXPORT void PLy_exception_set_with_details(PyObject *excclass, ErrorData *edata);
+
+#endif /* PLPY_ELOG_H */
diff --git a/src/pl/plpython/plpy_exec.c b/src/pl/plpython/plpy_exec.c
new file mode 100644
index 0000000..e06fde1
--- /dev/null
+++ b/src/pl/plpython/plpy_exec.c
@@ -0,0 +1,1098 @@
+/*
+ * executing Python code
+ *
+ * src/pl/plpython/plpy_exec.c
+ */
+
+#include "postgres.h"
+
+#include "access/htup_details.h"
+#include "access/xact.h"
+#include "catalog/pg_type.h"
+#include "commands/trigger.h"
+#include "executor/spi.h"
+#include "funcapi.h"
+#include "plpy_elog.h"
+#include "plpy_exec.h"
+#include "plpy_main.h"
+#include "plpy_procedure.h"
+#include "plpy_subxactobject.h"
+#include "plpython.h"
+#include "utils/builtins.h"
+#include "utils/lsyscache.h"
+#include "utils/rel.h"
+#include "utils/typcache.h"
+
+/* saved state for a set-returning function */
+typedef struct PLySRFState
+{
+ PyObject *iter; /* Python iterator producing results */
+ PLySavedArgs *savedargs; /* function argument values */
+ MemoryContextCallback callback; /* for releasing refcounts when done */
+} PLySRFState;
+
+static PyObject *PLy_function_build_args(FunctionCallInfo fcinfo, PLyProcedure *proc);
+static PLySavedArgs *PLy_function_save_args(PLyProcedure *proc);
+static void PLy_function_restore_args(PLyProcedure *proc, PLySavedArgs *savedargs);
+static void PLy_function_drop_args(PLySavedArgs *savedargs);
+static void PLy_global_args_push(PLyProcedure *proc);
+static void PLy_global_args_pop(PLyProcedure *proc);
+static void plpython_srf_cleanup_callback(void *arg);
+static void plpython_return_error_callback(void *arg);
+
+static PyObject *PLy_trigger_build_args(FunctionCallInfo fcinfo, PLyProcedure *proc,
+ HeapTuple *rv);
+static HeapTuple PLy_modify_tuple(PLyProcedure *proc, PyObject *pltd,
+ TriggerData *tdata, HeapTuple otup);
+static void plpython_trigger_error_callback(void *arg);
+
+static PyObject *PLy_procedure_call(PLyProcedure *proc, const char *kargs, PyObject *vargs);
+static void PLy_abort_open_subtransactions(int save_subxact_level);
+
+
+/* function subhandler */
+Datum
+PLy_exec_function(FunctionCallInfo fcinfo, PLyProcedure *proc)
+{
+ bool is_setof = proc->is_setof;
+ Datum rv;
+ PyObject *volatile plargs = NULL;
+ PyObject *volatile plrv = NULL;
+ FuncCallContext *volatile funcctx = NULL;
+ PLySRFState *volatile srfstate = NULL;
+ ErrorContextCallback plerrcontext;
+
+ /*
+ * If the function is called recursively, we must push outer-level
+ * arguments into the stack. This must be immediately before the PG_TRY
+ * to ensure that the corresponding pop happens.
+ */
+ PLy_global_args_push(proc);
+
+ PG_TRY();
+ {
+ if (is_setof)
+ {
+ /* First Call setup */
+ if (SRF_IS_FIRSTCALL())
+ {
+ funcctx = SRF_FIRSTCALL_INIT();
+ srfstate = (PLySRFState *)
+ MemoryContextAllocZero(funcctx->multi_call_memory_ctx,
+ sizeof(PLySRFState));
+ /* Immediately register cleanup callback */
+ srfstate->callback.func = plpython_srf_cleanup_callback;
+ srfstate->callback.arg = (void *) srfstate;
+ MemoryContextRegisterResetCallback(funcctx->multi_call_memory_ctx,
+ &srfstate->callback);
+ funcctx->user_fctx = (void *) srfstate;
+ }
+ /* Every call setup */
+ funcctx = SRF_PERCALL_SETUP();
+ Assert(funcctx != NULL);
+ srfstate = (PLySRFState *) funcctx->user_fctx;
+ Assert(srfstate != NULL);
+ }
+
+ if (srfstate == NULL || srfstate->iter == NULL)
+ {
+ /*
+ * Non-SETOF function or first time for SETOF function: build
+ * args, then actually execute the function.
+ */
+ plargs = PLy_function_build_args(fcinfo, proc);
+ plrv = PLy_procedure_call(proc, "args", plargs);
+ Assert(plrv != NULL);
+ }
+ else
+ {
+ /*
+ * Second or later call for a SETOF function: restore arguments in
+ * globals dict to what they were when we left off. We must do
+ * this in case multiple evaluations of the same SETOF function
+ * are interleaved. It's a bit annoying, since the iterator may
+ * not look at the arguments at all, but we have no way to know
+ * that. Fortunately this isn't terribly expensive.
+ */
+ if (srfstate->savedargs)
+ PLy_function_restore_args(proc, srfstate->savedargs);
+ srfstate->savedargs = NULL; /* deleted by restore_args */
+ }
+
+ /*
+ * If it returns a set, call the iterator to get the next return item.
+ * We stay in the SPI context while doing this, because PyIter_Next()
+ * calls back into Python code which might contain SPI calls.
+ */
+ if (is_setof)
+ {
+ if (srfstate->iter == NULL)
+ {
+ /* first time -- do checks and setup */
+ ReturnSetInfo *rsi = (ReturnSetInfo *) fcinfo->resultinfo;
+
+ if (!rsi || !IsA(rsi, ReturnSetInfo) ||
+ (rsi->allowedModes & SFRM_ValuePerCall) == 0)
+ {
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("unsupported set function return mode"),
+ errdetail("PL/Python set-returning functions only support returning one value per call.")));
+ }
+ rsi->returnMode = SFRM_ValuePerCall;
+
+ /* Make iterator out of returned object */
+ srfstate->iter = PyObject_GetIter(plrv);
+
+ Py_DECREF(plrv);
+ plrv = NULL;
+
+ if (srfstate->iter == NULL)
+ ereport(ERROR,
+ (errcode(ERRCODE_DATATYPE_MISMATCH),
+ errmsg("returned object cannot be iterated"),
+ errdetail("PL/Python set-returning functions must return an iterable object.")));
+ }
+
+ /* Fetch next from iterator */
+ plrv = PyIter_Next(srfstate->iter);
+ if (plrv == NULL)
+ {
+ /* Iterator is exhausted or error happened */
+ bool has_error = (PyErr_Occurred() != NULL);
+
+ Py_DECREF(srfstate->iter);
+ srfstate->iter = NULL;
+
+ if (has_error)
+ PLy_elog(ERROR, "error fetching next item from iterator");
+
+ /* Pass a null through the data-returning steps below */
+ Py_INCREF(Py_None);
+ plrv = Py_None;
+ }
+ else
+ {
+ /*
+ * This won't be last call, so save argument values. We do
+ * this again each time in case the iterator is changing those
+ * values.
+ */
+ srfstate->savedargs = PLy_function_save_args(proc);
+ }
+ }
+
+ /*
+ * Disconnect from SPI manager and then create the return values datum
+ * (if the input function does a palloc for it this must not be
+ * allocated in the SPI memory context because SPI_finish would free
+ * it).
+ */
+ if (SPI_finish() != SPI_OK_FINISH)
+ elog(ERROR, "SPI_finish failed");
+
+ plerrcontext.callback = plpython_return_error_callback;
+ plerrcontext.previous = error_context_stack;
+ error_context_stack = &plerrcontext;
+
+ /*
+ * For a procedure or function declared to return void, the Python
+ * return value must be None. For void-returning functions, we also
+ * treat a None return value as a special "void datum" rather than
+ * NULL (as is the case for non-void-returning functions).
+ */
+ if (proc->result.typoid == VOIDOID)
+ {
+ if (plrv != Py_None)
+ {
+ if (proc->is_procedure)
+ ereport(ERROR,
+ (errcode(ERRCODE_DATATYPE_MISMATCH),
+ errmsg("PL/Python procedure did not return None")));
+ else
+ ereport(ERROR,
+ (errcode(ERRCODE_DATATYPE_MISMATCH),
+ errmsg("PL/Python function with return type \"void\" did not return None")));
+ }
+
+ fcinfo->isnull = false;
+ rv = (Datum) 0;
+ }
+ else if (plrv == Py_None &&
+ srfstate && srfstate->iter == NULL)
+ {
+ /*
+ * In a SETOF function, the iteration-ending null isn't a real
+ * value; don't pass it through the input function, which might
+ * complain.
+ */
+ fcinfo->isnull = true;
+ rv = (Datum) 0;
+ }
+ else
+ {
+ /* Normal conversion of result */
+ rv = PLy_output_convert(&proc->result, plrv,
+ &fcinfo->isnull);
+ }
+ }
+ PG_CATCH();
+ {
+ /* Pop old arguments from the stack if they were pushed above */
+ PLy_global_args_pop(proc);
+
+ Py_XDECREF(plargs);
+ Py_XDECREF(plrv);
+
+ /*
+ * If there was an error within a SRF, the iterator might not have
+ * been exhausted yet. Clear it so the next invocation of the
+ * function will start the iteration again. (This code is probably
+ * unnecessary now; plpython_srf_cleanup_callback should take care of
+ * cleanup. But it doesn't hurt anything to do it here.)
+ */
+ if (srfstate)
+ {
+ Py_XDECREF(srfstate->iter);
+ srfstate->iter = NULL;
+ /* And drop any saved args; we won't need them */
+ if (srfstate->savedargs)
+ PLy_function_drop_args(srfstate->savedargs);
+ srfstate->savedargs = NULL;
+ }
+
+ PG_RE_THROW();
+ }
+ PG_END_TRY();
+
+ error_context_stack = plerrcontext.previous;
+
+ /* Pop old arguments from the stack if they were pushed above */
+ PLy_global_args_pop(proc);
+
+ Py_XDECREF(plargs);
+ Py_DECREF(plrv);
+
+ if (srfstate)
+ {
+ /* We're in a SRF, exit appropriately */
+ if (srfstate->iter == NULL)
+ {
+ /* Iterator exhausted, so we're done */
+ SRF_RETURN_DONE(funcctx);
+ }
+ else if (fcinfo->isnull)
+ SRF_RETURN_NEXT_NULL(funcctx);
+ else
+ SRF_RETURN_NEXT(funcctx, rv);
+ }
+
+ /* Plain function, just return the Datum value (possibly null) */
+ return rv;
+}
+
+/* trigger subhandler
+ *
+ * the python function is expected to return Py_None if the tuple is
+ * acceptable and unmodified. Otherwise it should return a PyUnicode
+ * object who's value is SKIP, or MODIFY. SKIP means don't perform
+ * this action. MODIFY means the tuple has been modified, so update
+ * tuple and perform action. SKIP and MODIFY assume the trigger fires
+ * BEFORE the event and is ROW level. postgres expects the function
+ * to take no arguments and return an argument of type trigger.
+ */
+HeapTuple
+PLy_exec_trigger(FunctionCallInfo fcinfo, PLyProcedure *proc)
+{
+ HeapTuple rv = NULL;
+ PyObject *volatile plargs = NULL;
+ PyObject *volatile plrv = NULL;
+ TriggerData *tdata;
+ TupleDesc rel_descr;
+
+ Assert(CALLED_AS_TRIGGER(fcinfo));
+ tdata = (TriggerData *) fcinfo->context;
+
+ /*
+ * Input/output conversion for trigger tuples. We use the result and
+ * result_in fields to store the tuple conversion info. We do this over
+ * again on each call to cover the possibility that the relation's tupdesc
+ * changed since the trigger was last called. The PLy_xxx_setup_func
+ * calls should only happen once, but PLy_input_setup_tuple and
+ * PLy_output_setup_tuple are responsible for not doing repetitive work.
+ */
+ rel_descr = RelationGetDescr(tdata->tg_relation);
+ if (proc->result.typoid != rel_descr->tdtypeid)
+ PLy_output_setup_func(&proc->result, proc->mcxt,
+ rel_descr->tdtypeid,
+ rel_descr->tdtypmod,
+ proc);
+ if (proc->result_in.typoid != rel_descr->tdtypeid)
+ PLy_input_setup_func(&proc->result_in, proc->mcxt,
+ rel_descr->tdtypeid,
+ rel_descr->tdtypmod,
+ proc);
+ PLy_output_setup_tuple(&proc->result, rel_descr, proc);
+ PLy_input_setup_tuple(&proc->result_in, rel_descr, proc);
+
+ PG_TRY();
+ {
+ int rc PG_USED_FOR_ASSERTS_ONLY;
+
+ rc = SPI_register_trigger_data(tdata);
+ Assert(rc >= 0);
+
+ plargs = PLy_trigger_build_args(fcinfo, proc, &rv);
+ plrv = PLy_procedure_call(proc, "TD", plargs);
+
+ Assert(plrv != NULL);
+
+ /*
+ * Disconnect from SPI manager
+ */
+ if (SPI_finish() != SPI_OK_FINISH)
+ elog(ERROR, "SPI_finish failed");
+
+ /*
+ * return of None means we're happy with the tuple
+ */
+ if (plrv != Py_None)
+ {
+ char *srv;
+
+ if (PyUnicode_Check(plrv))
+ srv = PLyUnicode_AsString(plrv);
+ else
+ {
+ ereport(ERROR,
+ (errcode(ERRCODE_DATA_EXCEPTION),
+ errmsg("unexpected return value from trigger procedure"),
+ errdetail("Expected None or a string.")));
+ srv = NULL; /* keep compiler quiet */
+ }
+
+ if (pg_strcasecmp(srv, "SKIP") == 0)
+ rv = NULL;
+ else if (pg_strcasecmp(srv, "MODIFY") == 0)
+ {
+ if (TRIGGER_FIRED_BY_INSERT(tdata->tg_event) ||
+ TRIGGER_FIRED_BY_UPDATE(tdata->tg_event))
+ rv = PLy_modify_tuple(proc, plargs, tdata, rv);
+ else
+ ereport(WARNING,
+ (errmsg("PL/Python trigger function returned \"MODIFY\" in a DELETE trigger -- ignored")));
+ }
+ else if (pg_strcasecmp(srv, "OK") != 0)
+ {
+ /*
+ * accept "OK" as an alternative to None; otherwise, raise an
+ * error
+ */
+ ereport(ERROR,
+ (errcode(ERRCODE_DATA_EXCEPTION),
+ errmsg("unexpected return value from trigger procedure"),
+ errdetail("Expected None, \"OK\", \"SKIP\", or \"MODIFY\".")));
+ }
+ }
+ }
+ PG_FINALLY();
+ {
+ Py_XDECREF(plargs);
+ Py_XDECREF(plrv);
+ }
+ PG_END_TRY();
+
+ return rv;
+}
+
+/* helper functions for Python code execution */
+
+static PyObject *
+PLy_function_build_args(FunctionCallInfo fcinfo, PLyProcedure *proc)
+{
+ PyObject *volatile arg = NULL;
+ PyObject *args;
+ int i;
+
+ /*
+ * Make any Py*_New() calls before the PG_TRY block so that we can quickly
+ * return NULL on failure. We can't return within the PG_TRY block, else
+ * we'd miss unwinding the exception stack.
+ */
+ args = PyList_New(proc->nargs);
+ if (!args)
+ return NULL;
+
+ PG_TRY();
+ {
+ for (i = 0; i < proc->nargs; i++)
+ {
+ PLyDatumToOb *arginfo = &proc->args[i];
+
+ if (fcinfo->args[i].isnull)
+ arg = NULL;
+ else
+ arg = PLy_input_convert(arginfo, fcinfo->args[i].value);
+
+ if (arg == NULL)
+ {
+ Py_INCREF(Py_None);
+ arg = Py_None;
+ }
+
+ if (PyList_SetItem(args, i, arg) == -1)
+ PLy_elog(ERROR, "PyList_SetItem() failed, while setting up arguments");
+
+ if (proc->argnames && proc->argnames[i] &&
+ PyDict_SetItemString(proc->globals, proc->argnames[i], arg) == -1)
+ PLy_elog(ERROR, "PyDict_SetItemString() failed, while setting up arguments");
+ arg = NULL;
+ }
+
+ /* Set up output conversion for functions returning RECORD */
+ if (proc->result.typoid == RECORDOID)
+ {
+ TupleDesc desc;
+
+ if (get_call_result_type(fcinfo, NULL, &desc) != TYPEFUNC_COMPOSITE)
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("function returning record called in context "
+ "that cannot accept type record")));
+
+ /* cache the output conversion functions */
+ PLy_output_setup_record(&proc->result, desc, proc);
+ }
+ }
+ PG_CATCH();
+ {
+ Py_XDECREF(arg);
+ Py_XDECREF(args);
+
+ PG_RE_THROW();
+ }
+ PG_END_TRY();
+
+ return args;
+}
+
+/*
+ * Construct a PLySavedArgs struct representing the current values of the
+ * procedure's arguments in its globals dict. This can be used to restore
+ * those values when exiting a recursive call level or returning control to a
+ * set-returning function.
+ *
+ * This would not be necessary except for an ancient decision to make args
+ * available via the proc's globals :-( ... but we're stuck with that now.
+ */
+static PLySavedArgs *
+PLy_function_save_args(PLyProcedure *proc)
+{
+ PLySavedArgs *result;
+
+ /* saved args are always allocated in procedure's context */
+ result = (PLySavedArgs *)
+ MemoryContextAllocZero(proc->mcxt,
+ offsetof(PLySavedArgs, namedargs) +
+ proc->nargs * sizeof(PyObject *));
+ result->nargs = proc->nargs;
+
+ /* Fetch the "args" list */
+ result->args = PyDict_GetItemString(proc->globals, "args");
+ Py_XINCREF(result->args);
+
+ /* Fetch all the named arguments */
+ if (proc->argnames)
+ {
+ int i;
+
+ for (i = 0; i < result->nargs; i++)
+ {
+ if (proc->argnames[i])
+ {
+ result->namedargs[i] = PyDict_GetItemString(proc->globals,
+ proc->argnames[i]);
+ Py_XINCREF(result->namedargs[i]);
+ }
+ }
+ }
+
+ return result;
+}
+
+/*
+ * Restore procedure's arguments from a PLySavedArgs struct,
+ * then free the struct.
+ */
+static void
+PLy_function_restore_args(PLyProcedure *proc, PLySavedArgs *savedargs)
+{
+ /* Restore named arguments into their slots in the globals dict */
+ if (proc->argnames)
+ {
+ int i;
+
+ for (i = 0; i < savedargs->nargs; i++)
+ {
+ if (proc->argnames[i] && savedargs->namedargs[i])
+ {
+ PyDict_SetItemString(proc->globals, proc->argnames[i],
+ savedargs->namedargs[i]);
+ Py_DECREF(savedargs->namedargs[i]);
+ }
+ }
+ }
+
+ /* Restore the "args" object, too */
+ if (savedargs->args)
+ {
+ PyDict_SetItemString(proc->globals, "args", savedargs->args);
+ Py_DECREF(savedargs->args);
+ }
+
+ /* And free the PLySavedArgs struct */
+ pfree(savedargs);
+}
+
+/*
+ * Free a PLySavedArgs struct without restoring the values.
+ */
+static void
+PLy_function_drop_args(PLySavedArgs *savedargs)
+{
+ int i;
+
+ /* Drop references for named args */
+ for (i = 0; i < savedargs->nargs; i++)
+ {
+ Py_XDECREF(savedargs->namedargs[i]);
+ }
+
+ /* Drop ref to the "args" object, too */
+ Py_XDECREF(savedargs->args);
+
+ /* And free the PLySavedArgs struct */
+ pfree(savedargs);
+}
+
+/*
+ * Save away any existing arguments for the given procedure, so that we can
+ * install new values for a recursive call. This should be invoked before
+ * doing PLy_function_build_args().
+ *
+ * NB: caller must ensure that PLy_global_args_pop gets invoked once, and
+ * only once, per successful completion of PLy_global_args_push. Otherwise
+ * we'll end up out-of-sync between the actual call stack and the contents
+ * of proc->argstack.
+ */
+static void
+PLy_global_args_push(PLyProcedure *proc)
+{
+ /* We only need to push if we are already inside some active call */
+ if (proc->calldepth > 0)
+ {
+ PLySavedArgs *node;
+
+ /* Build a struct containing current argument values */
+ node = PLy_function_save_args(proc);
+
+ /*
+ * Push the saved argument values into the procedure's stack. Once we
+ * modify either proc->argstack or proc->calldepth, we had better
+ * return without the possibility of error.
+ */
+ node->next = proc->argstack;
+ proc->argstack = node;
+ }
+ proc->calldepth++;
+}
+
+/*
+ * Pop old arguments when exiting a recursive call.
+ *
+ * Note: the idea here is to adjust the proc's callstack state before doing
+ * anything that could possibly fail. In event of any error, we want the
+ * callstack to look like we've done the pop. Leaking a bit of memory is
+ * tolerable.
+ */
+static void
+PLy_global_args_pop(PLyProcedure *proc)
+{
+ Assert(proc->calldepth > 0);
+ /* We only need to pop if we were already inside some active call */
+ if (proc->calldepth > 1)
+ {
+ PLySavedArgs *ptr = proc->argstack;
+
+ /* Pop the callstack */
+ Assert(ptr != NULL);
+ proc->argstack = ptr->next;
+ proc->calldepth--;
+
+ /* Restore argument values, then free ptr */
+ PLy_function_restore_args(proc, ptr);
+ }
+ else
+ {
+ /* Exiting call depth 1 */
+ Assert(proc->argstack == NULL);
+ proc->calldepth--;
+
+ /*
+ * We used to delete the named arguments (but not "args") from the
+ * proc's globals dict when exiting the outermost call level for a
+ * function. This seems rather pointless though: nothing can see the
+ * dict until the function is called again, at which time we'll
+ * overwrite those dict entries. So don't bother with that.
+ */
+ }
+}
+
+/*
+ * Memory context deletion callback for cleaning up a PLySRFState.
+ * We need this in case execution of the SRF is terminated early,
+ * due to error or the caller simply not running it to completion.
+ */
+static void
+plpython_srf_cleanup_callback(void *arg)
+{
+ PLySRFState *srfstate = (PLySRFState *) arg;
+
+ /* Release refcount on the iter, if we still have one */
+ Py_XDECREF(srfstate->iter);
+ srfstate->iter = NULL;
+ /* And drop any saved args; we won't need them */
+ if (srfstate->savedargs)
+ PLy_function_drop_args(srfstate->savedargs);
+ srfstate->savedargs = NULL;
+}
+
+static void
+plpython_return_error_callback(void *arg)
+{
+ PLyExecutionContext *exec_ctx = PLy_current_execution_context();
+
+ if (exec_ctx->curr_proc &&
+ !exec_ctx->curr_proc->is_procedure)
+ errcontext("while creating return value");
+}
+
+static PyObject *
+PLy_trigger_build_args(FunctionCallInfo fcinfo, PLyProcedure *proc, HeapTuple *rv)
+{
+ TriggerData *tdata = (TriggerData *) fcinfo->context;
+ TupleDesc rel_descr = RelationGetDescr(tdata->tg_relation);
+ PyObject *pltname,
+ *pltevent,
+ *pltwhen,
+ *pltlevel,
+ *pltrelid,
+ *plttablename,
+ *plttableschema,
+ *pltargs = NULL,
+ *pytnew,
+ *pytold,
+ *pltdata;
+ char *stroid;
+
+ /*
+ * Make any Py*_New() calls before the PG_TRY block so that we can quickly
+ * return NULL on failure. We can't return within the PG_TRY block, else
+ * we'd miss unwinding the exception stack.
+ */
+ pltdata = PyDict_New();
+ if (!pltdata)
+ return NULL;
+
+ if (tdata->tg_trigger->tgnargs)
+ {
+ pltargs = PyList_New(tdata->tg_trigger->tgnargs);
+ if (!pltargs)
+ {
+ Py_DECREF(pltdata);
+ return NULL;
+ }
+ }
+
+ PG_TRY();
+ {
+ pltname = PLyUnicode_FromString(tdata->tg_trigger->tgname);
+ PyDict_SetItemString(pltdata, "name", pltname);
+ Py_DECREF(pltname);
+
+ stroid = DatumGetCString(DirectFunctionCall1(oidout,
+ ObjectIdGetDatum(tdata->tg_relation->rd_id)));
+ pltrelid = PLyUnicode_FromString(stroid);
+ PyDict_SetItemString(pltdata, "relid", pltrelid);
+ Py_DECREF(pltrelid);
+ pfree(stroid);
+
+ stroid = SPI_getrelname(tdata->tg_relation);
+ plttablename = PLyUnicode_FromString(stroid);
+ PyDict_SetItemString(pltdata, "table_name", plttablename);
+ Py_DECREF(plttablename);
+ pfree(stroid);
+
+ stroid = SPI_getnspname(tdata->tg_relation);
+ plttableschema = PLyUnicode_FromString(stroid);
+ PyDict_SetItemString(pltdata, "table_schema", plttableschema);
+ Py_DECREF(plttableschema);
+ pfree(stroid);
+
+ if (TRIGGER_FIRED_BEFORE(tdata->tg_event))
+ pltwhen = PLyUnicode_FromString("BEFORE");
+ else if (TRIGGER_FIRED_AFTER(tdata->tg_event))
+ pltwhen = PLyUnicode_FromString("AFTER");
+ else if (TRIGGER_FIRED_INSTEAD(tdata->tg_event))
+ pltwhen = PLyUnicode_FromString("INSTEAD OF");
+ else
+ {
+ elog(ERROR, "unrecognized WHEN tg_event: %u", tdata->tg_event);
+ pltwhen = NULL; /* keep compiler quiet */
+ }
+ PyDict_SetItemString(pltdata, "when", pltwhen);
+ Py_DECREF(pltwhen);
+
+ if (TRIGGER_FIRED_FOR_ROW(tdata->tg_event))
+ {
+ pltlevel = PLyUnicode_FromString("ROW");
+ PyDict_SetItemString(pltdata, "level", pltlevel);
+ Py_DECREF(pltlevel);
+
+ /*
+ * Note: In BEFORE trigger, stored generated columns are not
+ * computed yet, so don't make them accessible in NEW row.
+ */
+
+ if (TRIGGER_FIRED_BY_INSERT(tdata->tg_event))
+ {
+ pltevent = PLyUnicode_FromString("INSERT");
+
+ PyDict_SetItemString(pltdata, "old", Py_None);
+ pytnew = PLy_input_from_tuple(&proc->result_in,
+ tdata->tg_trigtuple,
+ rel_descr,
+ !TRIGGER_FIRED_BEFORE(tdata->tg_event));
+ PyDict_SetItemString(pltdata, "new", pytnew);
+ Py_DECREF(pytnew);
+ *rv = tdata->tg_trigtuple;
+ }
+ else if (TRIGGER_FIRED_BY_DELETE(tdata->tg_event))
+ {
+ pltevent = PLyUnicode_FromString("DELETE");
+
+ PyDict_SetItemString(pltdata, "new", Py_None);
+ pytold = PLy_input_from_tuple(&proc->result_in,
+ tdata->tg_trigtuple,
+ rel_descr,
+ true);
+ PyDict_SetItemString(pltdata, "old", pytold);
+ Py_DECREF(pytold);
+ *rv = tdata->tg_trigtuple;
+ }
+ else if (TRIGGER_FIRED_BY_UPDATE(tdata->tg_event))
+ {
+ pltevent = PLyUnicode_FromString("UPDATE");
+
+ pytnew = PLy_input_from_tuple(&proc->result_in,
+ tdata->tg_newtuple,
+ rel_descr,
+ !TRIGGER_FIRED_BEFORE(tdata->tg_event));
+ PyDict_SetItemString(pltdata, "new", pytnew);
+ Py_DECREF(pytnew);
+ pytold = PLy_input_from_tuple(&proc->result_in,
+ tdata->tg_trigtuple,
+ rel_descr,
+ true);
+ PyDict_SetItemString(pltdata, "old", pytold);
+ Py_DECREF(pytold);
+ *rv = tdata->tg_newtuple;
+ }
+ else
+ {
+ elog(ERROR, "unrecognized OP tg_event: %u", tdata->tg_event);
+ pltevent = NULL; /* keep compiler quiet */
+ }
+
+ PyDict_SetItemString(pltdata, "event", pltevent);
+ Py_DECREF(pltevent);
+ }
+ else if (TRIGGER_FIRED_FOR_STATEMENT(tdata->tg_event))
+ {
+ pltlevel = PLyUnicode_FromString("STATEMENT");
+ PyDict_SetItemString(pltdata, "level", pltlevel);
+ Py_DECREF(pltlevel);
+
+ PyDict_SetItemString(pltdata, "old", Py_None);
+ PyDict_SetItemString(pltdata, "new", Py_None);
+ *rv = NULL;
+
+ if (TRIGGER_FIRED_BY_INSERT(tdata->tg_event))
+ pltevent = PLyUnicode_FromString("INSERT");
+ else if (TRIGGER_FIRED_BY_DELETE(tdata->tg_event))
+ pltevent = PLyUnicode_FromString("DELETE");
+ else if (TRIGGER_FIRED_BY_UPDATE(tdata->tg_event))
+ pltevent = PLyUnicode_FromString("UPDATE");
+ else if (TRIGGER_FIRED_BY_TRUNCATE(tdata->tg_event))
+ pltevent = PLyUnicode_FromString("TRUNCATE");
+ else
+ {
+ elog(ERROR, "unrecognized OP tg_event: %u", tdata->tg_event);
+ pltevent = NULL; /* keep compiler quiet */
+ }
+
+ PyDict_SetItemString(pltdata, "event", pltevent);
+ Py_DECREF(pltevent);
+ }
+ else
+ elog(ERROR, "unrecognized LEVEL tg_event: %u", tdata->tg_event);
+
+ if (tdata->tg_trigger->tgnargs)
+ {
+ /*
+ * all strings...
+ */
+ int i;
+ PyObject *pltarg;
+
+ /* pltargs should have been allocated before the PG_TRY block. */
+ Assert(pltargs);
+
+ for (i = 0; i < tdata->tg_trigger->tgnargs; i++)
+ {
+ pltarg = PLyUnicode_FromString(tdata->tg_trigger->tgargs[i]);
+
+ /*
+ * stolen, don't Py_DECREF
+ */
+ PyList_SetItem(pltargs, i, pltarg);
+ }
+ }
+ else
+ {
+ Py_INCREF(Py_None);
+ pltargs = Py_None;
+ }
+ PyDict_SetItemString(pltdata, "args", pltargs);
+ Py_DECREF(pltargs);
+ }
+ PG_CATCH();
+ {
+ Py_XDECREF(pltargs);
+ Py_XDECREF(pltdata);
+ PG_RE_THROW();
+ }
+ PG_END_TRY();
+
+ return pltdata;
+}
+
+/*
+ * Apply changes requested by a MODIFY return from a trigger function.
+ */
+static HeapTuple
+PLy_modify_tuple(PLyProcedure *proc, PyObject *pltd, TriggerData *tdata,
+ HeapTuple otup)
+{
+ HeapTuple rtup;
+ PyObject *volatile plntup;
+ PyObject *volatile plkeys;
+ PyObject *volatile plval;
+ Datum *volatile modvalues;
+ bool *volatile modnulls;
+ bool *volatile modrepls;
+ ErrorContextCallback plerrcontext;
+
+ plerrcontext.callback = plpython_trigger_error_callback;
+ plerrcontext.previous = error_context_stack;
+ error_context_stack = &plerrcontext;
+
+ plntup = plkeys = plval = NULL;
+ modvalues = NULL;
+ modnulls = NULL;
+ modrepls = NULL;
+
+ PG_TRY();
+ {
+ TupleDesc tupdesc;
+ int nkeys,
+ i;
+
+ if ((plntup = PyDict_GetItemString(pltd, "new")) == NULL)
+ ereport(ERROR,
+ (errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("TD[\"new\"] deleted, cannot modify row")));
+ Py_INCREF(plntup);
+ if (!PyDict_Check(plntup))
+ ereport(ERROR,
+ (errcode(ERRCODE_DATATYPE_MISMATCH),
+ errmsg("TD[\"new\"] is not a dictionary")));
+
+ plkeys = PyDict_Keys(plntup);
+ nkeys = PyList_Size(plkeys);
+
+ tupdesc = RelationGetDescr(tdata->tg_relation);
+
+ modvalues = (Datum *) palloc0(tupdesc->natts * sizeof(Datum));
+ modnulls = (bool *) palloc0(tupdesc->natts * sizeof(bool));
+ modrepls = (bool *) palloc0(tupdesc->natts * sizeof(bool));
+
+ for (i = 0; i < nkeys; i++)
+ {
+ PyObject *platt;
+ char *plattstr;
+ int attn;
+ PLyObToDatum *att;
+
+ platt = PyList_GetItem(plkeys, i);
+ if (PyUnicode_Check(platt))
+ plattstr = PLyUnicode_AsString(platt);
+ else
+ {
+ ereport(ERROR,
+ (errcode(ERRCODE_DATATYPE_MISMATCH),
+ errmsg("TD[\"new\"] dictionary key at ordinal position %d is not a string", i)));
+ plattstr = NULL; /* keep compiler quiet */
+ }
+ attn = SPI_fnumber(tupdesc, plattstr);
+ if (attn == SPI_ERROR_NOATTRIBUTE)
+ ereport(ERROR,
+ (errcode(ERRCODE_UNDEFINED_COLUMN),
+ errmsg("key \"%s\" found in TD[\"new\"] does not exist as a column in the triggering row",
+ plattstr)));
+ if (attn <= 0)
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("cannot set system attribute \"%s\"",
+ plattstr)));
+ if (TupleDescAttr(tupdesc, attn - 1)->attgenerated)
+ ereport(ERROR,
+ (errcode(ERRCODE_E_R_I_E_TRIGGER_PROTOCOL_VIOLATED),
+ errmsg("cannot set generated column \"%s\"",
+ plattstr)));
+
+ plval = PyDict_GetItem(plntup, platt);
+ if (plval == NULL)
+ elog(FATAL, "Python interpreter is probably corrupted");
+
+ Py_INCREF(plval);
+
+ /* We assume proc->result is set up to convert tuples properly */
+ att = &proc->result.u.tuple.atts[attn - 1];
+
+ modvalues[attn - 1] = PLy_output_convert(att,
+ plval,
+ &modnulls[attn - 1]);
+ modrepls[attn - 1] = true;
+
+ Py_DECREF(plval);
+ plval = NULL;
+ }
+
+ rtup = heap_modify_tuple(otup, tupdesc, modvalues, modnulls, modrepls);
+ }
+ PG_CATCH();
+ {
+ Py_XDECREF(plntup);
+ Py_XDECREF(plkeys);
+ Py_XDECREF(plval);
+
+ if (modvalues)
+ pfree(modvalues);
+ if (modnulls)
+ pfree(modnulls);
+ if (modrepls)
+ pfree(modrepls);
+
+ PG_RE_THROW();
+ }
+ PG_END_TRY();
+
+ Py_DECREF(plntup);
+ Py_DECREF(plkeys);
+
+ pfree(modvalues);
+ pfree(modnulls);
+ pfree(modrepls);
+
+ error_context_stack = plerrcontext.previous;
+
+ return rtup;
+}
+
+static void
+plpython_trigger_error_callback(void *arg)
+{
+ PLyExecutionContext *exec_ctx = PLy_current_execution_context();
+
+ if (exec_ctx->curr_proc)
+ errcontext("while modifying trigger row");
+}
+
+/* execute Python code, propagate Python errors to the backend */
+static PyObject *
+PLy_procedure_call(PLyProcedure *proc, const char *kargs, PyObject *vargs)
+{
+ PyObject *rv = NULL;
+ int volatile save_subxact_level = list_length(explicit_subtransactions);
+
+ PyDict_SetItemString(proc->globals, kargs, vargs);
+
+ PG_TRY();
+ {
+#if PY_VERSION_HEX >= 0x03020000
+ rv = PyEval_EvalCode(proc->code,
+ proc->globals, proc->globals);
+#else
+ rv = PyEval_EvalCode((PyCodeObject *) proc->code,
+ proc->globals, proc->globals);
+#endif
+
+ /*
+ * Since plpy will only let you close subtransactions that you
+ * started, you cannot *unnest* subtransactions, only *nest* them
+ * without closing.
+ */
+ Assert(list_length(explicit_subtransactions) >= save_subxact_level);
+ }
+ PG_FINALLY();
+ {
+ PLy_abort_open_subtransactions(save_subxact_level);
+ }
+ PG_END_TRY();
+
+ /* If the Python code returned an error, propagate it */
+ if (rv == NULL)
+ PLy_elog(ERROR, NULL);
+
+ return rv;
+}
+
+/*
+ * Abort lingering subtransactions that have been explicitly started
+ * by plpy.subtransaction().start() and not properly closed.
+ */
+static void
+PLy_abort_open_subtransactions(int save_subxact_level)
+{
+ Assert(save_subxact_level >= 0);
+
+ while (list_length(explicit_subtransactions) > save_subxact_level)
+ {
+ PLySubtransactionData *subtransactiondata;
+
+ Assert(explicit_subtransactions != NIL);
+
+ ereport(WARNING,
+ (errmsg("forcibly aborting a subtransaction that has not been exited")));
+
+ RollbackAndReleaseCurrentSubTransaction();
+
+ subtransactiondata = (PLySubtransactionData *) linitial(explicit_subtransactions);
+ explicit_subtransactions = list_delete_first(explicit_subtransactions);
+
+ MemoryContextSwitchTo(subtransactiondata->oldcontext);
+ CurrentResourceOwner = subtransactiondata->oldowner;
+ pfree(subtransactiondata);
+ }
+}
diff --git a/src/pl/plpython/plpy_exec.h b/src/pl/plpython/plpy_exec.h
new file mode 100644
index 0000000..68da1ff
--- /dev/null
+++ b/src/pl/plpython/plpy_exec.h
@@ -0,0 +1,13 @@
+/*
+ * src/pl/plpython/plpy_exec.h
+ */
+
+#ifndef PLPY_EXEC_H
+#define PLPY_EXEC_H
+
+#include "plpy_procedure.h"
+
+extern Datum PLy_exec_function(FunctionCallInfo fcinfo, PLyProcedure *proc);
+extern HeapTuple PLy_exec_trigger(FunctionCallInfo fcinfo, PLyProcedure *proc);
+
+#endif /* PLPY_EXEC_H */
diff --git a/src/pl/plpython/plpy_main.c b/src/pl/plpython/plpy_main.c
new file mode 100644
index 0000000..010a973
--- /dev/null
+++ b/src/pl/plpython/plpy_main.c
@@ -0,0 +1,419 @@
+/*
+ * PL/Python main entry points
+ *
+ * src/pl/plpython/plpy_main.c
+ */
+
+#include "postgres.h"
+
+#include "access/htup_details.h"
+#include "catalog/pg_proc.h"
+#include "catalog/pg_type.h"
+#include "commands/trigger.h"
+#include "executor/spi.h"
+#include "miscadmin.h"
+#include "plpy_elog.h"
+#include "plpy_exec.h"
+#include "plpy_main.h"
+#include "plpy_plpymodule.h"
+#include "plpy_procedure.h"
+#include "plpy_subxactobject.h"
+#include "plpython.h"
+#include "utils/guc.h"
+#include "utils/memutils.h"
+#include "utils/rel.h"
+#include "utils/syscache.h"
+
+/*
+ * exported functions
+ */
+
+PG_MODULE_MAGIC;
+
+PG_FUNCTION_INFO_V1(plpython3_validator);
+PG_FUNCTION_INFO_V1(plpython3_call_handler);
+PG_FUNCTION_INFO_V1(plpython3_inline_handler);
+
+
+static bool PLy_procedure_is_trigger(Form_pg_proc procStruct);
+static void plpython_error_callback(void *arg);
+static void plpython_inline_error_callback(void *arg);
+static void PLy_init_interp(void);
+
+static PLyExecutionContext *PLy_push_execution_context(bool atomic_context);
+static void PLy_pop_execution_context(void);
+
+/* static state for Python library conflict detection */
+static int *plpython_version_bitmask_ptr = NULL;
+static int plpython_version_bitmask = 0;
+
+/* initialize global variables */
+PyObject *PLy_interp_globals = NULL;
+
+/* this doesn't need to be global; use PLy_current_execution_context() */
+static PLyExecutionContext *PLy_execution_contexts = NULL;
+
+
+void
+_PG_init(void)
+{
+ int **bitmask_ptr;
+
+ /*
+ * Set up a shared bitmask variable telling which Python version(s) are
+ * loaded into this process's address space. If there's more than one, we
+ * cannot call into libpython for fear of causing crashes. But postpone
+ * the actual failure for later, so that operations like pg_restore can
+ * load more than one plpython library so long as they don't try to do
+ * anything much with the language.
+ *
+ * While we only support Python 3 these days, somebody might create an
+ * out-of-tree version adding back support for Python 2. Conflicts with
+ * such an extension should be detected.
+ */
+ bitmask_ptr = (int **) find_rendezvous_variable("plpython_version_bitmask");
+ if (!(*bitmask_ptr)) /* am I the first? */
+ *bitmask_ptr = &plpython_version_bitmask;
+ /* Retain pointer to the agreed-on shared variable ... */
+ plpython_version_bitmask_ptr = *bitmask_ptr;
+ /* ... and announce my presence */
+ *plpython_version_bitmask_ptr |= (1 << PY_MAJOR_VERSION);
+
+ /*
+ * This should be safe even in the presence of conflicting plpythons, and
+ * it's necessary to do it before possibly throwing a conflict error, or
+ * the error message won't get localized.
+ */
+ pg_bindtextdomain(TEXTDOMAIN);
+}
+
+/*
+ * Perform one-time setup of PL/Python, after checking for a conflict
+ * with other versions of Python.
+ */
+static void
+PLy_initialize(void)
+{
+ static bool inited = false;
+
+ /*
+ * Check for multiple Python libraries before actively doing anything with
+ * libpython. This must be repeated on each entry to PL/Python, in case a
+ * conflicting library got loaded since we last looked.
+ *
+ * It is attractive to weaken this error from FATAL to ERROR, but there
+ * would be corner cases, so it seems best to be conservative.
+ */
+ if (*plpython_version_bitmask_ptr != (1 << PY_MAJOR_VERSION))
+ ereport(FATAL,
+ (errmsg("multiple Python libraries are present in session"),
+ errdetail("Only one Python major version can be used in one session.")));
+
+ /* The rest should only be done once per session */
+ if (inited)
+ return;
+
+ PyImport_AppendInittab("plpy", PyInit_plpy);
+ Py_Initialize();
+ PyImport_ImportModule("plpy");
+ PLy_init_interp();
+ PLy_init_plpy();
+ if (PyErr_Occurred())
+ PLy_elog(FATAL, "untrapped error in initialization");
+
+ init_procedure_caches();
+
+ explicit_subtransactions = NIL;
+
+ PLy_execution_contexts = NULL;
+
+ inited = true;
+}
+
+/*
+ * This should be called only once, from PLy_initialize. Initialize the Python
+ * interpreter and global data.
+ */
+static void
+PLy_init_interp(void)
+{
+ static PyObject *PLy_interp_safe_globals = NULL;
+ PyObject *mainmod;
+
+ mainmod = PyImport_AddModule("__main__");
+ if (mainmod == NULL || PyErr_Occurred())
+ PLy_elog(ERROR, "could not import \"__main__\" module");
+ Py_INCREF(mainmod);
+ PLy_interp_globals = PyModule_GetDict(mainmod);
+ PLy_interp_safe_globals = PyDict_New();
+ if (PLy_interp_safe_globals == NULL)
+ PLy_elog(ERROR, NULL);
+ PyDict_SetItemString(PLy_interp_globals, "GD", PLy_interp_safe_globals);
+ Py_DECREF(mainmod);
+ if (PLy_interp_globals == NULL || PyErr_Occurred())
+ PLy_elog(ERROR, "could not initialize globals");
+}
+
+Datum
+plpython3_validator(PG_FUNCTION_ARGS)
+{
+ Oid funcoid = PG_GETARG_OID(0);
+ HeapTuple tuple;
+ Form_pg_proc procStruct;
+ bool is_trigger;
+
+ if (!CheckFunctionValidatorAccess(fcinfo->flinfo->fn_oid, funcoid))
+ PG_RETURN_VOID();
+
+ if (!check_function_bodies)
+ PG_RETURN_VOID();
+
+ /* Do this only after making sure we need to do something */
+ PLy_initialize();
+
+ /* Get the new function's pg_proc entry */
+ tuple = SearchSysCache1(PROCOID, ObjectIdGetDatum(funcoid));
+ if (!HeapTupleIsValid(tuple))
+ elog(ERROR, "cache lookup failed for function %u", funcoid);
+ procStruct = (Form_pg_proc) GETSTRUCT(tuple);
+
+ is_trigger = PLy_procedure_is_trigger(procStruct);
+
+ ReleaseSysCache(tuple);
+
+ /* We can't validate triggers against any particular table ... */
+ PLy_procedure_get(funcoid, InvalidOid, is_trigger);
+
+ PG_RETURN_VOID();
+}
+
+Datum
+plpython3_call_handler(PG_FUNCTION_ARGS)
+{
+ bool nonatomic;
+ Datum retval;
+ PLyExecutionContext *exec_ctx;
+ ErrorContextCallback plerrcontext;
+
+ PLy_initialize();
+
+ nonatomic = fcinfo->context &&
+ IsA(fcinfo->context, CallContext) &&
+ !castNode(CallContext, fcinfo->context)->atomic;
+
+ /* Note: SPI_finish() happens in plpy_exec.c, which is dubious design */
+ if (SPI_connect_ext(nonatomic ? SPI_OPT_NONATOMIC : 0) != SPI_OK_CONNECT)
+ elog(ERROR, "SPI_connect failed");
+
+ /*
+ * Push execution context onto stack. It is important that this get
+ * popped again, so avoid putting anything that could throw error between
+ * here and the PG_TRY.
+ */
+ exec_ctx = PLy_push_execution_context(!nonatomic);
+
+ PG_TRY();
+ {
+ Oid funcoid = fcinfo->flinfo->fn_oid;
+ PLyProcedure *proc;
+
+ /*
+ * Setup error traceback support for ereport(). Note that the PG_TRY
+ * structure pops this for us again at exit, so we needn't do that
+ * explicitly, nor do we risk the callback getting called after we've
+ * destroyed the exec_ctx.
+ */
+ plerrcontext.callback = plpython_error_callback;
+ plerrcontext.arg = exec_ctx;
+ plerrcontext.previous = error_context_stack;
+ error_context_stack = &plerrcontext;
+
+ if (CALLED_AS_TRIGGER(fcinfo))
+ {
+ Relation tgrel = ((TriggerData *) fcinfo->context)->tg_relation;
+ HeapTuple trv;
+
+ proc = PLy_procedure_get(funcoid, RelationGetRelid(tgrel), true);
+ exec_ctx->curr_proc = proc;
+ trv = PLy_exec_trigger(fcinfo, proc);
+ retval = PointerGetDatum(trv);
+ }
+ else
+ {
+ proc = PLy_procedure_get(funcoid, InvalidOid, false);
+ exec_ctx->curr_proc = proc;
+ retval = PLy_exec_function(fcinfo, proc);
+ }
+ }
+ PG_CATCH();
+ {
+ PLy_pop_execution_context();
+ PyErr_Clear();
+ PG_RE_THROW();
+ }
+ PG_END_TRY();
+
+ /* Destroy the execution context */
+ PLy_pop_execution_context();
+
+ return retval;
+}
+
+Datum
+plpython3_inline_handler(PG_FUNCTION_ARGS)
+{
+ LOCAL_FCINFO(fake_fcinfo, 0);
+ InlineCodeBlock *codeblock = (InlineCodeBlock *) DatumGetPointer(PG_GETARG_DATUM(0));
+ FmgrInfo flinfo;
+ PLyProcedure proc;
+ PLyExecutionContext *exec_ctx;
+ ErrorContextCallback plerrcontext;
+
+ PLy_initialize();
+
+ /* Note: SPI_finish() happens in plpy_exec.c, which is dubious design */
+ if (SPI_connect_ext(codeblock->atomic ? 0 : SPI_OPT_NONATOMIC) != SPI_OK_CONNECT)
+ elog(ERROR, "SPI_connect failed");
+
+ MemSet(fcinfo, 0, SizeForFunctionCallInfo(0));
+ MemSet(&flinfo, 0, sizeof(flinfo));
+ fake_fcinfo->flinfo = &flinfo;
+ flinfo.fn_oid = InvalidOid;
+ flinfo.fn_mcxt = CurrentMemoryContext;
+
+ MemSet(&proc, 0, sizeof(PLyProcedure));
+ proc.mcxt = AllocSetContextCreate(TopMemoryContext,
+ "__plpython_inline_block",
+ ALLOCSET_DEFAULT_SIZES);
+ proc.pyname = MemoryContextStrdup(proc.mcxt, "__plpython_inline_block");
+ proc.langid = codeblock->langOid;
+
+ /*
+ * This is currently sufficient to get PLy_exec_function to work, but
+ * someday we might need to be honest and use PLy_output_setup_func.
+ */
+ proc.result.typoid = VOIDOID;
+
+ /*
+ * Push execution context onto stack. It is important that this get
+ * popped again, so avoid putting anything that could throw error between
+ * here and the PG_TRY.
+ */
+ exec_ctx = PLy_push_execution_context(codeblock->atomic);
+
+ PG_TRY();
+ {
+ /*
+ * Setup error traceback support for ereport().
+ * plpython_inline_error_callback doesn't currently need exec_ctx, but
+ * for consistency with plpython3_call_handler we do it the same way.
+ */
+ plerrcontext.callback = plpython_inline_error_callback;
+ plerrcontext.arg = exec_ctx;
+ plerrcontext.previous = error_context_stack;
+ error_context_stack = &plerrcontext;
+
+ PLy_procedure_compile(&proc, codeblock->source_text);
+ exec_ctx->curr_proc = &proc;
+ PLy_exec_function(fake_fcinfo, &proc);
+ }
+ PG_CATCH();
+ {
+ PLy_pop_execution_context();
+ PLy_procedure_delete(&proc);
+ PyErr_Clear();
+ PG_RE_THROW();
+ }
+ PG_END_TRY();
+
+ /* Destroy the execution context */
+ PLy_pop_execution_context();
+
+ /* Now clean up the transient procedure we made */
+ PLy_procedure_delete(&proc);
+
+ PG_RETURN_VOID();
+}
+
+static bool
+PLy_procedure_is_trigger(Form_pg_proc procStruct)
+{
+ return (procStruct->prorettype == TRIGGEROID);
+}
+
+static void
+plpython_error_callback(void *arg)
+{
+ PLyExecutionContext *exec_ctx = (PLyExecutionContext *) arg;
+
+ if (exec_ctx->curr_proc)
+ {
+ if (exec_ctx->curr_proc->is_procedure)
+ errcontext("PL/Python procedure \"%s\"",
+ PLy_procedure_name(exec_ctx->curr_proc));
+ else
+ errcontext("PL/Python function \"%s\"",
+ PLy_procedure_name(exec_ctx->curr_proc));
+ }
+}
+
+static void
+plpython_inline_error_callback(void *arg)
+{
+ errcontext("PL/Python anonymous code block");
+}
+
+PLyExecutionContext *
+PLy_current_execution_context(void)
+{
+ if (PLy_execution_contexts == NULL)
+ elog(ERROR, "no Python function is currently executing");
+
+ return PLy_execution_contexts;
+}
+
+MemoryContext
+PLy_get_scratch_context(PLyExecutionContext *context)
+{
+ /*
+ * A scratch context might never be needed in a given plpython procedure,
+ * so allocate it on first request.
+ */
+ if (context->scratch_ctx == NULL)
+ context->scratch_ctx =
+ AllocSetContextCreate(TopTransactionContext,
+ "PL/Python scratch context",
+ ALLOCSET_DEFAULT_SIZES);
+ return context->scratch_ctx;
+}
+
+static PLyExecutionContext *
+PLy_push_execution_context(bool atomic_context)
+{
+ PLyExecutionContext *context;
+
+ /* Pick a memory context similar to what SPI uses. */
+ context = (PLyExecutionContext *)
+ MemoryContextAlloc(atomic_context ? TopTransactionContext : PortalContext,
+ sizeof(PLyExecutionContext));
+ context->curr_proc = NULL;
+ context->scratch_ctx = NULL;
+ context->next = PLy_execution_contexts;
+ PLy_execution_contexts = context;
+ return context;
+}
+
+static void
+PLy_pop_execution_context(void)
+{
+ PLyExecutionContext *context = PLy_execution_contexts;
+
+ if (context == NULL)
+ elog(ERROR, "no Python function is currently executing");
+
+ PLy_execution_contexts = context->next;
+
+ if (context->scratch_ctx)
+ MemoryContextDelete(context->scratch_ctx);
+ pfree(context);
+}
diff --git a/src/pl/plpython/plpy_main.h b/src/pl/plpython/plpy_main.h
new file mode 100644
index 0000000..e8c97c2
--- /dev/null
+++ b/src/pl/plpython/plpy_main.h
@@ -0,0 +1,31 @@
+/*
+ * src/pl/plpython/plpy_main.h
+ */
+
+#ifndef PLPY_MAIN_H
+#define PLPY_MAIN_H
+
+#include "plpy_procedure.h"
+
+/* the interpreter's globals dict */
+extern PyObject *PLy_interp_globals;
+
+/*
+ * A stack of PL/Python execution contexts. Each time user-defined Python code
+ * is called, an execution context is created and put on the stack. After the
+ * Python code returns, the context is destroyed.
+ */
+typedef struct PLyExecutionContext
+{
+ PLyProcedure *curr_proc; /* the currently executing procedure */
+ MemoryContext scratch_ctx; /* a context for things like type I/O */
+ struct PLyExecutionContext *next; /* previous stack level */
+} PLyExecutionContext;
+
+/* Get the current execution context */
+extern PLyExecutionContext *PLy_current_execution_context(void);
+
+/* Get the scratch memory context for specified execution context */
+extern MemoryContext PLy_get_scratch_context(PLyExecutionContext *context);
+
+#endif /* PLPY_MAIN_H */
diff --git a/src/pl/plpython/plpy_planobject.c b/src/pl/plpython/plpy_planobject.c
new file mode 100644
index 0000000..ec2439c
--- /dev/null
+++ b/src/pl/plpython/plpy_planobject.c
@@ -0,0 +1,125 @@
+/*
+ * the PLyPlan class
+ *
+ * src/pl/plpython/plpy_planobject.c
+ */
+
+#include "postgres.h"
+
+#include "plpy_cursorobject.h"
+#include "plpy_elog.h"
+#include "plpy_planobject.h"
+#include "plpy_spi.h"
+#include "plpython.h"
+#include "utils/memutils.h"
+
+static void PLy_plan_dealloc(PyObject *arg);
+static PyObject *PLy_plan_cursor(PyObject *self, PyObject *args);
+static PyObject *PLy_plan_execute(PyObject *self, PyObject *args);
+static PyObject *PLy_plan_status(PyObject *self, PyObject *args);
+
+static char PLy_plan_doc[] = "Store a PostgreSQL plan";
+
+static PyMethodDef PLy_plan_methods[] = {
+ {"cursor", PLy_plan_cursor, METH_VARARGS, NULL},
+ {"execute", PLy_plan_execute, METH_VARARGS, NULL},
+ {"status", PLy_plan_status, METH_VARARGS, NULL},
+ {NULL, NULL, 0, NULL}
+};
+
+static PyTypeObject PLy_PlanType = {
+ PyVarObject_HEAD_INIT(NULL, 0)
+ .tp_name = "PLyPlan",
+ .tp_basicsize = sizeof(PLyPlanObject),
+ .tp_dealloc = PLy_plan_dealloc,
+ .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,
+ .tp_doc = PLy_plan_doc,
+ .tp_methods = PLy_plan_methods,
+};
+
+void
+PLy_plan_init_type(void)
+{
+ if (PyType_Ready(&PLy_PlanType) < 0)
+ elog(ERROR, "could not initialize PLy_PlanType");
+}
+
+PyObject *
+PLy_plan_new(void)
+{
+ PLyPlanObject *ob;
+
+ if ((ob = PyObject_New(PLyPlanObject, &PLy_PlanType)) == NULL)
+ return NULL;
+
+ ob->plan = NULL;
+ ob->nargs = 0;
+ ob->types = NULL;
+ ob->values = NULL;
+ ob->args = NULL;
+ ob->mcxt = NULL;
+
+ return (PyObject *) ob;
+}
+
+bool
+is_PLyPlanObject(PyObject *ob)
+{
+ return ob->ob_type == &PLy_PlanType;
+}
+
+static void
+PLy_plan_dealloc(PyObject *arg)
+{
+ PLyPlanObject *ob = (PLyPlanObject *) arg;
+
+ if (ob->plan)
+ {
+ SPI_freeplan(ob->plan);
+ ob->plan = NULL;
+ }
+ if (ob->mcxt)
+ {
+ MemoryContextDelete(ob->mcxt);
+ ob->mcxt = NULL;
+ }
+ arg->ob_type->tp_free(arg);
+}
+
+
+static PyObject *
+PLy_plan_cursor(PyObject *self, PyObject *args)
+{
+ PyObject *planargs = NULL;
+
+ if (!PyArg_ParseTuple(args, "|O", &planargs))
+ return NULL;
+
+ return PLy_cursor_plan(self, planargs);
+}
+
+
+static PyObject *
+PLy_plan_execute(PyObject *self, PyObject *args)
+{
+ PyObject *list = NULL;
+ long limit = 0;
+
+ if (!PyArg_ParseTuple(args, "|Ol", &list, &limit))
+ return NULL;
+
+ return PLy_spi_execute_plan(self, list, limit);
+}
+
+
+static PyObject *
+PLy_plan_status(PyObject *self, PyObject *args)
+{
+ if (PyArg_ParseTuple(args, ":status"))
+ {
+ Py_INCREF(Py_True);
+ return Py_True;
+ /* return PyLong_FromLong(self->status); */
+ }
+ return NULL;
+}
diff --git a/src/pl/plpython/plpy_planobject.h b/src/pl/plpython/plpy_planobject.h
new file mode 100644
index 0000000..729effb
--- /dev/null
+++ b/src/pl/plpython/plpy_planobject.h
@@ -0,0 +1,27 @@
+/*
+ * src/pl/plpython/plpy_planobject.h
+ */
+
+#ifndef PLPY_PLANOBJECT_H
+#define PLPY_PLANOBJECT_H
+
+#include "executor/spi.h"
+#include "plpy_typeio.h"
+
+
+typedef struct PLyPlanObject
+{
+ PyObject_HEAD
+ SPIPlanPtr plan;
+ int nargs;
+ Oid *types;
+ Datum *values;
+ PLyObToDatum *args;
+ MemoryContext mcxt;
+} PLyPlanObject;
+
+extern void PLy_plan_init_type(void);
+extern PyObject *PLy_plan_new(void);
+extern bool is_PLyPlanObject(PyObject *ob);
+
+#endif /* PLPY_PLANOBJECT_H */
diff --git a/src/pl/plpython/plpy_plpymodule.c b/src/pl/plpython/plpy_plpymodule.c
new file mode 100644
index 0000000..fa08f0d
--- /dev/null
+++ b/src/pl/plpython/plpy_plpymodule.c
@@ -0,0 +1,561 @@
+/*
+ * the plpy module
+ *
+ * src/pl/plpython/plpy_plpymodule.c
+ */
+
+#include "postgres.h"
+
+#include "access/xact.h"
+#include "mb/pg_wchar.h"
+#include "plpy_cursorobject.h"
+#include "plpy_elog.h"
+#include "plpy_main.h"
+#include "plpy_planobject.h"
+#include "plpy_plpymodule.h"
+#include "plpy_resultobject.h"
+#include "plpy_spi.h"
+#include "plpy_subxactobject.h"
+#include "plpython.h"
+#include "utils/builtins.h"
+#include "utils/snapmgr.h"
+
+HTAB *PLy_spi_exceptions = NULL;
+
+
+static void PLy_add_exceptions(PyObject *plpy);
+static PyObject *PLy_create_exception(char *name,
+ PyObject *base, PyObject *dict,
+ const char *modname, PyObject *mod);
+static void PLy_generate_spi_exceptions(PyObject *mod, PyObject *base);
+
+/* module functions */
+static PyObject *PLy_debug(PyObject *self, PyObject *args, PyObject *kw);
+static PyObject *PLy_log(PyObject *self, PyObject *args, PyObject *kw);
+static PyObject *PLy_info(PyObject *self, PyObject *args, PyObject *kw);
+static PyObject *PLy_notice(PyObject *self, PyObject *args, PyObject *kw);
+static PyObject *PLy_warning(PyObject *self, PyObject *args, PyObject *kw);
+static PyObject *PLy_error(PyObject *self, PyObject *args, PyObject *kw);
+static PyObject *PLy_fatal(PyObject *self, PyObject *args, PyObject *kw);
+static PyObject *PLy_quote_literal(PyObject *self, PyObject *args);
+static PyObject *PLy_quote_nullable(PyObject *self, PyObject *args);
+static PyObject *PLy_quote_ident(PyObject *self, PyObject *args);
+
+
+/* A list of all known exceptions, generated from backend/utils/errcodes.txt */
+typedef struct ExceptionMap
+{
+ char *name;
+ char *classname;
+ int sqlstate;
+} ExceptionMap;
+
+static const ExceptionMap exception_map[] = {
+#include "spiexceptions.h"
+ {NULL, NULL, 0}
+};
+
+static PyMethodDef PLy_methods[] = {
+ /*
+ * logging methods
+ */
+ {"debug", (PyCFunction) (pg_funcptr_t) PLy_debug, METH_VARARGS | METH_KEYWORDS, NULL},
+ {"log", (PyCFunction) (pg_funcptr_t) PLy_log, METH_VARARGS | METH_KEYWORDS, NULL},
+ {"info", (PyCFunction) (pg_funcptr_t) PLy_info, METH_VARARGS | METH_KEYWORDS, NULL},
+ {"notice", (PyCFunction) (pg_funcptr_t) PLy_notice, METH_VARARGS | METH_KEYWORDS, NULL},
+ {"warning", (PyCFunction) (pg_funcptr_t) PLy_warning, METH_VARARGS | METH_KEYWORDS, NULL},
+ {"error", (PyCFunction) (pg_funcptr_t) PLy_error, METH_VARARGS | METH_KEYWORDS, NULL},
+ {"fatal", (PyCFunction) (pg_funcptr_t) PLy_fatal, METH_VARARGS | METH_KEYWORDS, NULL},
+
+ /*
+ * create a stored plan
+ */
+ {"prepare", PLy_spi_prepare, METH_VARARGS, NULL},
+
+ /*
+ * execute a plan or query
+ */
+ {"execute", PLy_spi_execute, METH_VARARGS, NULL},
+
+ /*
+ * escaping strings
+ */
+ {"quote_literal", PLy_quote_literal, METH_VARARGS, NULL},
+ {"quote_nullable", PLy_quote_nullable, METH_VARARGS, NULL},
+ {"quote_ident", PLy_quote_ident, METH_VARARGS, NULL},
+
+ /*
+ * create the subtransaction context manager
+ */
+ {"subtransaction", PLy_subtransaction_new, METH_NOARGS, NULL},
+
+ /*
+ * create a cursor
+ */
+ {"cursor", PLy_cursor, METH_VARARGS, NULL},
+
+ /*
+ * transaction control
+ */
+ {"commit", PLy_commit, METH_NOARGS, NULL},
+ {"rollback", PLy_rollback, METH_NOARGS, NULL},
+
+ {NULL, NULL, 0, NULL}
+};
+
+static PyMethodDef PLy_exc_methods[] = {
+ {NULL, NULL, 0, NULL}
+};
+
+static PyModuleDef PLy_module = {
+ PyModuleDef_HEAD_INIT,
+ .m_name = "plpy",
+ .m_size = -1,
+ .m_methods = PLy_methods,
+};
+
+static PyModuleDef PLy_exc_module = {
+ PyModuleDef_HEAD_INIT,
+ .m_name = "spiexceptions",
+ .m_size = -1,
+ .m_methods = PLy_exc_methods,
+};
+
+/*
+ * Must have external linkage, because PyMODINIT_FUNC does dllexport on
+ * Windows-like platforms.
+ */
+PyMODINIT_FUNC
+PyInit_plpy(void)
+{
+ PyObject *m;
+
+ m = PyModule_Create(&PLy_module);
+ if (m == NULL)
+ return NULL;
+
+ PLy_add_exceptions(m);
+
+ return m;
+}
+
+void
+PLy_init_plpy(void)
+{
+ PyObject *main_mod,
+ *main_dict,
+ *plpy_mod;
+
+ /*
+ * initialize plpy module
+ */
+ PLy_plan_init_type();
+ PLy_result_init_type();
+ PLy_subtransaction_init_type();
+ PLy_cursor_init_type();
+
+ PyModule_Create(&PLy_module);
+
+ /* PyDict_SetItemString(plpy, "PlanType", (PyObject *) &PLy_PlanType); */
+
+ /*
+ * initialize main module, and add plpy
+ */
+ main_mod = PyImport_AddModule("__main__");
+ main_dict = PyModule_GetDict(main_mod);
+ plpy_mod = PyImport_AddModule("plpy");
+ if (plpy_mod == NULL)
+ PLy_elog(ERROR, "could not import \"plpy\" module");
+ PyDict_SetItemString(main_dict, "plpy", plpy_mod);
+ if (PyErr_Occurred())
+ PLy_elog(ERROR, "could not import \"plpy\" module");
+}
+
+static void
+PLy_add_exceptions(PyObject *plpy)
+{
+ PyObject *excmod;
+ HASHCTL hash_ctl;
+
+ excmod = PyModule_Create(&PLy_exc_module);
+ if (excmod == NULL)
+ PLy_elog(ERROR, "could not create the spiexceptions module");
+
+ /*
+ * PyModule_AddObject does not add a refcount to the object, for some odd
+ * reason; we must do that.
+ */
+ Py_INCREF(excmod);
+ if (PyModule_AddObject(plpy, "spiexceptions", excmod) < 0)
+ PLy_elog(ERROR, "could not add the spiexceptions module");
+
+ PLy_exc_error = PLy_create_exception("plpy.Error", NULL, NULL,
+ "Error", plpy);
+ PLy_exc_fatal = PLy_create_exception("plpy.Fatal", NULL, NULL,
+ "Fatal", plpy);
+ PLy_exc_spi_error = PLy_create_exception("plpy.SPIError", NULL, NULL,
+ "SPIError", plpy);
+
+ hash_ctl.keysize = sizeof(int);
+ hash_ctl.entrysize = sizeof(PLyExceptionEntry);
+ PLy_spi_exceptions = hash_create("PL/Python SPI exceptions", 256,
+ &hash_ctl, HASH_ELEM | HASH_BLOBS);
+
+ PLy_generate_spi_exceptions(excmod, PLy_exc_spi_error);
+}
+
+/*
+ * Create an exception object and add it to the module
+ */
+static PyObject *
+PLy_create_exception(char *name, PyObject *base, PyObject *dict,
+ const char *modname, PyObject *mod)
+{
+ PyObject *exc;
+
+ exc = PyErr_NewException(name, base, dict);
+ if (exc == NULL)
+ PLy_elog(ERROR, NULL);
+
+ /*
+ * PyModule_AddObject does not add a refcount to the object, for some odd
+ * reason; we must do that.
+ */
+ Py_INCREF(exc);
+ PyModule_AddObject(mod, modname, exc);
+
+ /*
+ * The caller will also store a pointer to the exception object in some
+ * permanent variable, so add another ref to account for that. This is
+ * probably excessively paranoid, but let's be sure.
+ */
+ Py_INCREF(exc);
+ return exc;
+}
+
+/*
+ * Add all the autogenerated exceptions as subclasses of SPIError
+ */
+static void
+PLy_generate_spi_exceptions(PyObject *mod, PyObject *base)
+{
+ int i;
+
+ for (i = 0; exception_map[i].name != NULL; i++)
+ {
+ bool found;
+ PyObject *exc;
+ PLyExceptionEntry *entry;
+ PyObject *sqlstate;
+ PyObject *dict = PyDict_New();
+
+ if (dict == NULL)
+ PLy_elog(ERROR, NULL);
+
+ sqlstate = PLyUnicode_FromString(unpack_sql_state(exception_map[i].sqlstate));
+ if (sqlstate == NULL)
+ PLy_elog(ERROR, "could not generate SPI exceptions");
+
+ PyDict_SetItemString(dict, "sqlstate", sqlstate);
+ Py_DECREF(sqlstate);
+
+ exc = PLy_create_exception(exception_map[i].name, base, dict,
+ exception_map[i].classname, mod);
+
+ entry = hash_search(PLy_spi_exceptions, &exception_map[i].sqlstate,
+ HASH_ENTER, &found);
+ Assert(!found);
+ entry->exc = exc;
+ }
+}
+
+
+/*
+ * the python interface to the elog function
+ * don't confuse these with PLy_elog
+ */
+static PyObject *PLy_output(volatile int level, PyObject *self,
+ PyObject *args, PyObject *kw);
+
+static PyObject *
+PLy_debug(PyObject *self, PyObject *args, PyObject *kw)
+{
+ return PLy_output(DEBUG2, self, args, kw);
+}
+
+static PyObject *
+PLy_log(PyObject *self, PyObject *args, PyObject *kw)
+{
+ return PLy_output(LOG, self, args, kw);
+}
+
+static PyObject *
+PLy_info(PyObject *self, PyObject *args, PyObject *kw)
+{
+ return PLy_output(INFO, self, args, kw);
+}
+
+static PyObject *
+PLy_notice(PyObject *self, PyObject *args, PyObject *kw)
+{
+ return PLy_output(NOTICE, self, args, kw);
+}
+
+static PyObject *
+PLy_warning(PyObject *self, PyObject *args, PyObject *kw)
+{
+ return PLy_output(WARNING, self, args, kw);
+}
+
+static PyObject *
+PLy_error(PyObject *self, PyObject *args, PyObject *kw)
+{
+ return PLy_output(ERROR, self, args, kw);
+}
+
+static PyObject *
+PLy_fatal(PyObject *self, PyObject *args, PyObject *kw)
+{
+ return PLy_output(FATAL, self, args, kw);
+}
+
+static PyObject *
+PLy_quote_literal(PyObject *self, PyObject *args)
+{
+ const char *str;
+ char *quoted;
+ PyObject *ret;
+
+ if (!PyArg_ParseTuple(args, "s:quote_literal", &str))
+ return NULL;
+
+ quoted = quote_literal_cstr(str);
+ ret = PLyUnicode_FromString(quoted);
+ pfree(quoted);
+
+ return ret;
+}
+
+static PyObject *
+PLy_quote_nullable(PyObject *self, PyObject *args)
+{
+ const char *str;
+ char *quoted;
+ PyObject *ret;
+
+ if (!PyArg_ParseTuple(args, "z:quote_nullable", &str))
+ return NULL;
+
+ if (str == NULL)
+ return PLyUnicode_FromString("NULL");
+
+ quoted = quote_literal_cstr(str);
+ ret = PLyUnicode_FromString(quoted);
+ pfree(quoted);
+
+ return ret;
+}
+
+static PyObject *
+PLy_quote_ident(PyObject *self, PyObject *args)
+{
+ const char *str;
+ const char *quoted;
+ PyObject *ret;
+
+ if (!PyArg_ParseTuple(args, "s:quote_ident", &str))
+ return NULL;
+
+ quoted = quote_identifier(str);
+ ret = PLyUnicode_FromString(quoted);
+
+ return ret;
+}
+
+/* enforce cast of object to string */
+static char *
+object_to_string(PyObject *obj)
+{
+ if (obj)
+ {
+ PyObject *so = PyObject_Str(obj);
+
+ if (so != NULL)
+ {
+ char *str;
+
+ str = pstrdup(PLyUnicode_AsString(so));
+ Py_DECREF(so);
+
+ return str;
+ }
+ }
+
+ return NULL;
+}
+
+static PyObject *
+PLy_output(volatile int level, PyObject *self, PyObject *args, PyObject *kw)
+{
+ int sqlstate = 0;
+ char *volatile sqlstatestr = NULL;
+ char *volatile message = NULL;
+ char *volatile detail = NULL;
+ char *volatile hint = NULL;
+ char *volatile column_name = NULL;
+ char *volatile constraint_name = NULL;
+ char *volatile datatype_name = NULL;
+ char *volatile table_name = NULL;
+ char *volatile schema_name = NULL;
+ volatile MemoryContext oldcontext;
+ PyObject *key,
+ *value;
+ PyObject *volatile so;
+ Py_ssize_t pos = 0;
+
+ if (PyTuple_Size(args) == 1)
+ {
+ /*
+ * Treat single argument specially to avoid undesirable ('tuple',)
+ * decoration.
+ */
+ PyObject *o;
+
+ if (!PyArg_UnpackTuple(args, "plpy.elog", 1, 1, &o))
+ PLy_elog(ERROR, "could not unpack arguments in plpy.elog");
+ so = PyObject_Str(o);
+ }
+ else
+ so = PyObject_Str(args);
+
+ if (so == NULL || ((message = PLyUnicode_AsString(so)) == NULL))
+ {
+ level = ERROR;
+ message = dgettext(TEXTDOMAIN, "could not parse error message in plpy.elog");
+ }
+ message = pstrdup(message);
+
+ Py_XDECREF(so);
+
+ if (kw != NULL)
+ {
+ while (PyDict_Next(kw, &pos, &key, &value))
+ {
+ char *keyword = PLyUnicode_AsString(key);
+
+ if (strcmp(keyword, "message") == 0)
+ {
+ /* the message should not be overwritten */
+ if (PyTuple_Size(args) != 0)
+ {
+ PLy_exception_set(PyExc_TypeError, "argument 'message' given by name and position");
+ return NULL;
+ }
+
+ if (message)
+ pfree(message);
+ message = object_to_string(value);
+ }
+ else if (strcmp(keyword, "detail") == 0)
+ detail = object_to_string(value);
+ else if (strcmp(keyword, "hint") == 0)
+ hint = object_to_string(value);
+ else if (strcmp(keyword, "sqlstate") == 0)
+ sqlstatestr = object_to_string(value);
+ else if (strcmp(keyword, "schema_name") == 0)
+ schema_name = object_to_string(value);
+ else if (strcmp(keyword, "table_name") == 0)
+ table_name = object_to_string(value);
+ else if (strcmp(keyword, "column_name") == 0)
+ column_name = object_to_string(value);
+ else if (strcmp(keyword, "datatype_name") == 0)
+ datatype_name = object_to_string(value);
+ else if (strcmp(keyword, "constraint_name") == 0)
+ constraint_name = object_to_string(value);
+ else
+ {
+ PLy_exception_set(PyExc_TypeError,
+ "'%s' is an invalid keyword argument for this function",
+ keyword);
+ return NULL;
+ }
+ }
+ }
+
+ if (sqlstatestr != NULL)
+ {
+ if (strlen(sqlstatestr) != 5)
+ {
+ PLy_exception_set(PyExc_ValueError, "invalid SQLSTATE code");
+ return NULL;
+ }
+
+ if (strspn(sqlstatestr, "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ") != 5)
+ {
+ PLy_exception_set(PyExc_ValueError, "invalid SQLSTATE code");
+ return NULL;
+ }
+
+ sqlstate = MAKE_SQLSTATE(sqlstatestr[0],
+ sqlstatestr[1],
+ sqlstatestr[2],
+ sqlstatestr[3],
+ sqlstatestr[4]);
+ }
+
+ oldcontext = CurrentMemoryContext;
+ PG_TRY();
+ {
+ if (message != NULL)
+ pg_verifymbstr(message, strlen(message), false);
+ if (detail != NULL)
+ pg_verifymbstr(detail, strlen(detail), false);
+ if (hint != NULL)
+ pg_verifymbstr(hint, strlen(hint), false);
+ if (schema_name != NULL)
+ pg_verifymbstr(schema_name, strlen(schema_name), false);
+ if (table_name != NULL)
+ pg_verifymbstr(table_name, strlen(table_name), false);
+ if (column_name != NULL)
+ pg_verifymbstr(column_name, strlen(column_name), false);
+ if (datatype_name != NULL)
+ pg_verifymbstr(datatype_name, strlen(datatype_name), false);
+ if (constraint_name != NULL)
+ pg_verifymbstr(constraint_name, strlen(constraint_name), false);
+
+ ereport(level,
+ ((sqlstate != 0) ? errcode(sqlstate) : 0,
+ (message != NULL) ? errmsg_internal("%s", message) : 0,
+ (detail != NULL) ? errdetail_internal("%s", detail) : 0,
+ (hint != NULL) ? errhint("%s", hint) : 0,
+ (column_name != NULL) ?
+ err_generic_string(PG_DIAG_COLUMN_NAME, column_name) : 0,
+ (constraint_name != NULL) ?
+ err_generic_string(PG_DIAG_CONSTRAINT_NAME, constraint_name) : 0,
+ (datatype_name != NULL) ?
+ err_generic_string(PG_DIAG_DATATYPE_NAME, datatype_name) : 0,
+ (table_name != NULL) ?
+ err_generic_string(PG_DIAG_TABLE_NAME, table_name) : 0,
+ (schema_name != NULL) ?
+ err_generic_string(PG_DIAG_SCHEMA_NAME, schema_name) : 0));
+ }
+ PG_CATCH();
+ {
+ ErrorData *edata;
+
+ MemoryContextSwitchTo(oldcontext);
+ edata = CopyErrorData();
+ FlushErrorState();
+
+ PLy_exception_set_with_details(PLy_exc_error, edata);
+ FreeErrorData(edata);
+
+ return NULL;
+ }
+ PG_END_TRY();
+
+ /*
+ * return a legal object so the interpreter will continue on its merry way
+ */
+ Py_RETURN_NONE;
+}
diff --git a/src/pl/plpython/plpy_plpymodule.h b/src/pl/plpython/plpy_plpymodule.h
new file mode 100644
index 0000000..1ca3823
--- /dev/null
+++ b/src/pl/plpython/plpy_plpymodule.h
@@ -0,0 +1,18 @@
+/*
+ * src/pl/plpython/plpy_plpymodule.h
+ */
+
+#ifndef PLPY_PLPYMODULE_H
+#define PLPY_PLPYMODULE_H
+
+#include "plpython.h"
+#include "utils/hsearch.h"
+
+/* A hash table mapping sqlstates to exceptions, for speedy lookup */
+extern HTAB *PLy_spi_exceptions;
+
+
+PyMODINIT_FUNC PyInit_plpy(void);
+extern void PLy_init_plpy(void);
+
+#endif /* PLPY_PLPYMODULE_H */
diff --git a/src/pl/plpython/plpy_procedure.c b/src/pl/plpython/plpy_procedure.c
new file mode 100644
index 0000000..79b6ef6
--- /dev/null
+++ b/src/pl/plpython/plpy_procedure.c
@@ -0,0 +1,470 @@
+/*
+ * Python procedure manipulation for plpython
+ *
+ * src/pl/plpython/plpy_procedure.c
+ */
+
+#include "postgres.h"
+
+#include "access/htup_details.h"
+#include "access/transam.h"
+#include "funcapi.h"
+#include "catalog/pg_proc.h"
+#include "catalog/pg_type.h"
+#include "plpy_elog.h"
+#include "plpy_main.h"
+#include "plpy_procedure.h"
+#include "plpython.h"
+#include "utils/builtins.h"
+#include "utils/hsearch.h"
+#include "utils/inval.h"
+#include "utils/lsyscache.h"
+#include "utils/memutils.h"
+#include "utils/syscache.h"
+
+static HTAB *PLy_procedure_cache = NULL;
+
+static PLyProcedure *PLy_procedure_create(HeapTuple procTup, Oid fn_oid, bool is_trigger);
+static bool PLy_procedure_valid(PLyProcedure *proc, HeapTuple procTup);
+static char *PLy_procedure_munge_source(const char *name, const char *src);
+
+
+void
+init_procedure_caches(void)
+{
+ HASHCTL hash_ctl;
+
+ hash_ctl.keysize = sizeof(PLyProcedureKey);
+ hash_ctl.entrysize = sizeof(PLyProcedureEntry);
+ PLy_procedure_cache = hash_create("PL/Python procedures", 32, &hash_ctl,
+ HASH_ELEM | HASH_BLOBS);
+}
+
+/*
+ * PLy_procedure_name: get the name of the specified procedure.
+ *
+ * NB: this returns the SQL name, not the internal Python procedure name
+ */
+char *
+PLy_procedure_name(PLyProcedure *proc)
+{
+ if (proc == NULL)
+ return "<unknown procedure>";
+ return proc->proname;
+}
+
+/*
+ * PLy_procedure_get: returns a cached PLyProcedure, or creates, stores and
+ * returns a new PLyProcedure.
+ *
+ * fn_oid is the OID of the function requested
+ * fn_rel is InvalidOid or the relation this function triggers on
+ * is_trigger denotes whether the function is a trigger function
+ *
+ * The reason that both fn_rel and is_trigger need to be passed is that when
+ * trigger functions get validated we don't know which relation(s) they'll
+ * be used with, so no sensible fn_rel can be passed.
+ */
+PLyProcedure *
+PLy_procedure_get(Oid fn_oid, Oid fn_rel, bool is_trigger)
+{
+ bool use_cache = !(is_trigger && fn_rel == InvalidOid);
+ HeapTuple procTup;
+ PLyProcedureKey key;
+ PLyProcedureEntry *volatile entry = NULL;
+ PLyProcedure *volatile proc = NULL;
+ bool found = false;
+
+ procTup = SearchSysCache1(PROCOID, ObjectIdGetDatum(fn_oid));
+ if (!HeapTupleIsValid(procTup))
+ elog(ERROR, "cache lookup failed for function %u", fn_oid);
+
+ /*
+ * Look for the function in the cache, unless we don't have the necessary
+ * information (e.g. during validation). In that case we just don't cache
+ * anything.
+ */
+ if (use_cache)
+ {
+ key.fn_oid = fn_oid;
+ key.fn_rel = fn_rel;
+ entry = hash_search(PLy_procedure_cache, &key, HASH_ENTER, &found);
+ proc = entry->proc;
+ }
+
+ PG_TRY();
+ {
+ if (!found)
+ {
+ /* Haven't found it, create a new procedure */
+ proc = PLy_procedure_create(procTup, fn_oid, is_trigger);
+ if (use_cache)
+ entry->proc = proc;
+ }
+ else if (!PLy_procedure_valid(proc, procTup))
+ {
+ /* Found it, but it's invalid, free and reuse the cache entry */
+ entry->proc = NULL;
+ if (proc)
+ PLy_procedure_delete(proc);
+ proc = PLy_procedure_create(procTup, fn_oid, is_trigger);
+ entry->proc = proc;
+ }
+ /* Found it and it's valid, it's fine to use it */
+ }
+ PG_CATCH();
+ {
+ /* Do not leave an uninitialized entry in the cache */
+ if (use_cache)
+ hash_search(PLy_procedure_cache, &key, HASH_REMOVE, NULL);
+ PG_RE_THROW();
+ }
+ PG_END_TRY();
+
+ ReleaseSysCache(procTup);
+
+ return proc;
+}
+
+/*
+ * Create a new PLyProcedure structure
+ */
+static PLyProcedure *
+PLy_procedure_create(HeapTuple procTup, Oid fn_oid, bool is_trigger)
+{
+ char procName[NAMEDATALEN + 256];
+ Form_pg_proc procStruct;
+ PLyProcedure *volatile proc;
+ MemoryContext cxt;
+ MemoryContext oldcxt;
+ int rv;
+ char *ptr;
+
+ procStruct = (Form_pg_proc) GETSTRUCT(procTup);
+ rv = snprintf(procName, sizeof(procName),
+ "__plpython_procedure_%s_%u",
+ NameStr(procStruct->proname),
+ fn_oid);
+ if (rv >= sizeof(procName) || rv < 0)
+ elog(ERROR, "procedure name would overrun buffer");
+
+ /* Replace any not-legal-in-Python-names characters with '_' */
+ for (ptr = procName; *ptr; ptr++)
+ {
+ if (!((*ptr >= 'A' && *ptr <= 'Z') ||
+ (*ptr >= 'a' && *ptr <= 'z') ||
+ (*ptr >= '0' && *ptr <= '9')))
+ *ptr = '_';
+ }
+
+ /* Create long-lived context that all procedure info will live in */
+ cxt = AllocSetContextCreate(TopMemoryContext,
+ "PL/Python function",
+ ALLOCSET_DEFAULT_SIZES);
+
+ oldcxt = MemoryContextSwitchTo(cxt);
+
+ proc = (PLyProcedure *) palloc0(sizeof(PLyProcedure));
+ proc->mcxt = cxt;
+
+ PG_TRY();
+ {
+ Datum protrftypes_datum;
+ Datum prosrcdatum;
+ bool isnull;
+ char *procSource;
+ int i;
+
+ proc->proname = pstrdup(NameStr(procStruct->proname));
+ MemoryContextSetIdentifier(cxt, proc->proname);
+ proc->pyname = pstrdup(procName);
+ proc->fn_xmin = HeapTupleHeaderGetRawXmin(procTup->t_data);
+ proc->fn_tid = procTup->t_self;
+ proc->fn_readonly = (procStruct->provolatile != PROVOLATILE_VOLATILE);
+ proc->is_setof = procStruct->proretset;
+ proc->is_procedure = (procStruct->prokind == PROKIND_PROCEDURE);
+ proc->src = NULL;
+ proc->argnames = NULL;
+ proc->args = NULL;
+ proc->nargs = 0;
+ proc->langid = procStruct->prolang;
+ protrftypes_datum = SysCacheGetAttr(PROCOID, procTup,
+ Anum_pg_proc_protrftypes,
+ &isnull);
+ proc->trftypes = isnull ? NIL : oid_array_to_list(protrftypes_datum);
+ proc->code = NULL;
+ proc->statics = NULL;
+ proc->globals = NULL;
+ proc->calldepth = 0;
+ proc->argstack = NULL;
+
+ /*
+ * get information required for output conversion of the return value,
+ * but only if this isn't a trigger.
+ */
+ if (!is_trigger)
+ {
+ Oid rettype = procStruct->prorettype;
+ HeapTuple rvTypeTup;
+ Form_pg_type rvTypeStruct;
+
+ rvTypeTup = SearchSysCache1(TYPEOID, ObjectIdGetDatum(rettype));
+ if (!HeapTupleIsValid(rvTypeTup))
+ elog(ERROR, "cache lookup failed for type %u", rettype);
+ rvTypeStruct = (Form_pg_type) GETSTRUCT(rvTypeTup);
+
+ /* Disallow pseudotype result, except for void or record */
+ if (rvTypeStruct->typtype == TYPTYPE_PSEUDO)
+ {
+ if (rettype == VOIDOID ||
+ rettype == RECORDOID)
+ /* okay */ ;
+ else if (rettype == TRIGGEROID || rettype == EVENT_TRIGGEROID)
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("trigger functions can only be called as triggers")));
+ else
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("PL/Python functions cannot return type %s",
+ format_type_be(rettype))));
+ }
+
+ /* set up output function for procedure result */
+ PLy_output_setup_func(&proc->result, proc->mcxt,
+ rettype, -1, proc);
+
+ ReleaseSysCache(rvTypeTup);
+ }
+ else
+ {
+ /*
+ * In a trigger function, we use proc->result and proc->result_in
+ * for converting tuples, but we don't yet have enough info to set
+ * them up. PLy_exec_trigger will deal with it.
+ */
+ proc->result.typoid = InvalidOid;
+ proc->result_in.typoid = InvalidOid;
+ }
+
+ /*
+ * Now get information required for input conversion of the
+ * procedure's arguments. Note that we ignore output arguments here.
+ * If the function returns record, those I/O functions will be set up
+ * when the function is first called.
+ */
+ if (procStruct->pronargs)
+ {
+ Oid *types;
+ char **names,
+ *modes;
+ int pos,
+ total;
+
+ /* extract argument type info from the pg_proc tuple */
+ total = get_func_arg_info(procTup, &types, &names, &modes);
+
+ /* count number of in+inout args into proc->nargs */
+ if (modes == NULL)
+ proc->nargs = total;
+ else
+ {
+ /* proc->nargs was initialized to 0 above */
+ for (i = 0; i < total; i++)
+ {
+ if (modes[i] != PROARGMODE_OUT &&
+ modes[i] != PROARGMODE_TABLE)
+ (proc->nargs)++;
+ }
+ }
+
+ /* Allocate arrays for per-input-argument data */
+ proc->argnames = (char **) palloc0(sizeof(char *) * proc->nargs);
+ proc->args = (PLyDatumToOb *) palloc0(sizeof(PLyDatumToOb) * proc->nargs);
+
+ for (i = pos = 0; i < total; i++)
+ {
+ HeapTuple argTypeTup;
+ Form_pg_type argTypeStruct;
+
+ if (modes &&
+ (modes[i] == PROARGMODE_OUT ||
+ modes[i] == PROARGMODE_TABLE))
+ continue; /* skip OUT arguments */
+
+ Assert(types[i] == procStruct->proargtypes.values[pos]);
+
+ argTypeTup = SearchSysCache1(TYPEOID,
+ ObjectIdGetDatum(types[i]));
+ if (!HeapTupleIsValid(argTypeTup))
+ elog(ERROR, "cache lookup failed for type %u", types[i]);
+ argTypeStruct = (Form_pg_type) GETSTRUCT(argTypeTup);
+
+ /* disallow pseudotype arguments */
+ if (argTypeStruct->typtype == TYPTYPE_PSEUDO)
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("PL/Python functions cannot accept type %s",
+ format_type_be(types[i]))));
+
+ /* set up I/O function info */
+ PLy_input_setup_func(&proc->args[pos], proc->mcxt,
+ types[i], -1, /* typmod not known */
+ proc);
+
+ /* get argument name */
+ proc->argnames[pos] = names ? pstrdup(names[i]) : NULL;
+
+ ReleaseSysCache(argTypeTup);
+
+ pos++;
+ }
+ }
+
+ /*
+ * get the text of the function.
+ */
+ prosrcdatum = SysCacheGetAttrNotNull(PROCOID, procTup,
+ Anum_pg_proc_prosrc);
+ procSource = TextDatumGetCString(prosrcdatum);
+
+ PLy_procedure_compile(proc, procSource);
+
+ pfree(procSource);
+ }
+ PG_CATCH();
+ {
+ MemoryContextSwitchTo(oldcxt);
+ PLy_procedure_delete(proc);
+ PG_RE_THROW();
+ }
+ PG_END_TRY();
+
+ MemoryContextSwitchTo(oldcxt);
+ return proc;
+}
+
+/*
+ * Insert the procedure into the Python interpreter
+ */
+void
+PLy_procedure_compile(PLyProcedure *proc, const char *src)
+{
+ PyObject *crv = NULL;
+ char *msrc;
+
+ proc->globals = PyDict_Copy(PLy_interp_globals);
+
+ /*
+ * SD is private preserved data between calls. GD is global data shared by
+ * all functions
+ */
+ proc->statics = PyDict_New();
+ if (!proc->statics)
+ PLy_elog(ERROR, NULL);
+ PyDict_SetItemString(proc->globals, "SD", proc->statics);
+
+ /*
+ * insert the function code into the interpreter
+ */
+ msrc = PLy_procedure_munge_source(proc->pyname, src);
+ /* Save the mangled source for later inclusion in tracebacks */
+ proc->src = MemoryContextStrdup(proc->mcxt, msrc);
+ crv = PyRun_String(msrc, Py_file_input, proc->globals, NULL);
+ pfree(msrc);
+
+ if (crv != NULL)
+ {
+ int clen;
+ char call[NAMEDATALEN + 256];
+
+ Py_DECREF(crv);
+
+ /*
+ * compile a call to the function
+ */
+ clen = snprintf(call, sizeof(call), "%s()", proc->pyname);
+ if (clen < 0 || clen >= sizeof(call))
+ elog(ERROR, "string would overflow buffer");
+ proc->code = Py_CompileString(call, "<string>", Py_eval_input);
+ if (proc->code != NULL)
+ return;
+ }
+
+ if (proc->proname)
+ PLy_elog(ERROR, "could not compile PL/Python function \"%s\"",
+ proc->proname);
+ else
+ PLy_elog(ERROR, "could not compile anonymous PL/Python code block");
+}
+
+void
+PLy_procedure_delete(PLyProcedure *proc)
+{
+ Py_XDECREF(proc->code);
+ Py_XDECREF(proc->statics);
+ Py_XDECREF(proc->globals);
+ MemoryContextDelete(proc->mcxt);
+}
+
+/*
+ * Decide whether a cached PLyProcedure struct is still valid
+ */
+static bool
+PLy_procedure_valid(PLyProcedure *proc, HeapTuple procTup)
+{
+ if (proc == NULL)
+ return false;
+
+ /* If the pg_proc tuple has changed, it's not valid */
+ if (!(proc->fn_xmin == HeapTupleHeaderGetRawXmin(procTup->t_data) &&
+ ItemPointerEquals(&proc->fn_tid, &procTup->t_self)))
+ return false;
+
+ return true;
+}
+
+static char *
+PLy_procedure_munge_source(const char *name, const char *src)
+{
+ char *mrc,
+ *mp;
+ const char *sp;
+ size_t mlen;
+ int plen;
+
+ /*
+ * room for function source and the def statement
+ */
+ mlen = (strlen(src) * 2) + strlen(name) + 16;
+
+ mrc = palloc(mlen);
+ plen = snprintf(mrc, mlen, "def %s():\n\t", name);
+ Assert(plen >= 0 && plen < mlen);
+
+ sp = src;
+ mp = mrc + plen;
+
+ while (*sp != '\0')
+ {
+ if (*sp == '\r' && *(sp + 1) == '\n')
+ sp++;
+
+ if (*sp == '\n' || *sp == '\r')
+ {
+ *mp++ = '\n';
+ *mp++ = '\t';
+ sp++;
+ }
+ else
+ *mp++ = *sp++;
+ }
+ *mp++ = '\n';
+ *mp++ = '\n';
+ *mp = '\0';
+
+ if (mp > (mrc + mlen))
+ elog(FATAL, "buffer overrun in PLy_procedure_munge_source");
+
+ return mrc;
+}
diff --git a/src/pl/plpython/plpy_procedure.h b/src/pl/plpython/plpy_procedure.h
new file mode 100644
index 0000000..8968b5c
--- /dev/null
+++ b/src/pl/plpython/plpy_procedure.h
@@ -0,0 +1,70 @@
+/*
+ * src/pl/plpython/plpy_procedure.h
+ */
+
+#ifndef PLPY_PROCEDURE_H
+#define PLPY_PROCEDURE_H
+
+#include "plpy_typeio.h"
+
+
+extern void init_procedure_caches(void);
+
+
+/* saved arguments for outer recursion level or set-returning function */
+typedef struct PLySavedArgs
+{
+ struct PLySavedArgs *next; /* linked-list pointer */
+ PyObject *args; /* "args" element of globals dict */
+ int nargs; /* length of namedargs array */
+ PyObject *namedargs[FLEXIBLE_ARRAY_MEMBER]; /* named args */
+} PLySavedArgs;
+
+/* cached procedure data */
+typedef struct PLyProcedure
+{
+ MemoryContext mcxt; /* context holding this PLyProcedure and its
+ * subsidiary data */
+ char *proname; /* SQL name of procedure */
+ char *pyname; /* Python name of procedure */
+ TransactionId fn_xmin;
+ ItemPointerData fn_tid;
+ bool fn_readonly;
+ bool is_setof; /* true, if function returns result set */
+ bool is_procedure;
+ PLyObToDatum result; /* Function result output conversion info */
+ PLyDatumToOb result_in; /* For converting input tuples in a trigger */
+ char *src; /* textual procedure code, after mangling */
+ char **argnames; /* Argument names */
+ PLyDatumToOb *args; /* Argument input conversion info */
+ int nargs; /* Number of elements in above arrays */
+ Oid langid; /* OID of plpython pg_language entry */
+ List *trftypes; /* OID list of transform types */
+ PyObject *code; /* compiled procedure code */
+ PyObject *statics; /* data saved across calls, local scope */
+ PyObject *globals; /* data saved across calls, global scope */
+ long calldepth; /* depth of recursive calls of function */
+ PLySavedArgs *argstack; /* stack of outer-level call arguments */
+} PLyProcedure;
+
+/* the procedure cache key */
+typedef struct PLyProcedureKey
+{
+ Oid fn_oid; /* function OID */
+ Oid fn_rel; /* triggered-on relation or InvalidOid */
+} PLyProcedureKey;
+
+/* the procedure cache entry */
+typedef struct PLyProcedureEntry
+{
+ PLyProcedureKey key; /* hash key */
+ PLyProcedure *proc;
+} PLyProcedureEntry;
+
+/* PLyProcedure manipulation */
+extern char *PLy_procedure_name(PLyProcedure *proc);
+extern PLyProcedure *PLy_procedure_get(Oid fn_oid, Oid fn_rel, bool is_trigger);
+extern void PLy_procedure_compile(PLyProcedure *proc, const char *src);
+extern void PLy_procedure_delete(PLyProcedure *proc);
+
+#endif /* PLPY_PROCEDURE_H */
diff --git a/src/pl/plpython/plpy_resultobject.c b/src/pl/plpython/plpy_resultobject.c
new file mode 100644
index 0000000..95acce6
--- /dev/null
+++ b/src/pl/plpython/plpy_resultobject.c
@@ -0,0 +1,250 @@
+/*
+ * the PLyResult class
+ *
+ * src/pl/plpython/plpy_resultobject.c
+ */
+
+#include "postgres.h"
+
+#include "plpy_elog.h"
+#include "plpy_resultobject.h"
+#include "plpython.h"
+
+static void PLy_result_dealloc(PyObject *arg);
+static PyObject *PLy_result_colnames(PyObject *self, PyObject *unused);
+static PyObject *PLy_result_coltypes(PyObject *self, PyObject *unused);
+static PyObject *PLy_result_coltypmods(PyObject *self, PyObject *unused);
+static PyObject *PLy_result_nrows(PyObject *self, PyObject *args);
+static PyObject *PLy_result_status(PyObject *self, PyObject *args);
+static Py_ssize_t PLy_result_length(PyObject *arg);
+static PyObject *PLy_result_item(PyObject *arg, Py_ssize_t idx);
+static PyObject *PLy_result_str(PyObject *arg);
+static PyObject *PLy_result_subscript(PyObject *arg, PyObject *item);
+static int PLy_result_ass_subscript(PyObject *arg, PyObject *item, PyObject *value);
+
+static char PLy_result_doc[] = "Results of a PostgreSQL query";
+
+static PySequenceMethods PLy_result_as_sequence = {
+ .sq_length = PLy_result_length,
+ .sq_item = PLy_result_item,
+};
+
+static PyMappingMethods PLy_result_as_mapping = {
+ .mp_length = PLy_result_length,
+ .mp_subscript = PLy_result_subscript,
+ .mp_ass_subscript = PLy_result_ass_subscript,
+};
+
+static PyMethodDef PLy_result_methods[] = {
+ {"colnames", PLy_result_colnames, METH_NOARGS, NULL},
+ {"coltypes", PLy_result_coltypes, METH_NOARGS, NULL},
+ {"coltypmods", PLy_result_coltypmods, METH_NOARGS, NULL},
+ {"nrows", PLy_result_nrows, METH_VARARGS, NULL},
+ {"status", PLy_result_status, METH_VARARGS, NULL},
+ {NULL, NULL, 0, NULL}
+};
+
+static PyTypeObject PLy_ResultType = {
+ PyVarObject_HEAD_INIT(NULL, 0)
+ .tp_name = "PLyResult",
+ .tp_basicsize = sizeof(PLyResultObject),
+ .tp_dealloc = PLy_result_dealloc,
+ .tp_as_sequence = &PLy_result_as_sequence,
+ .tp_as_mapping = &PLy_result_as_mapping,
+ .tp_str = &PLy_result_str,
+ .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,
+ .tp_doc = PLy_result_doc,
+ .tp_methods = PLy_result_methods,
+};
+
+void
+PLy_result_init_type(void)
+{
+ if (PyType_Ready(&PLy_ResultType) < 0)
+ elog(ERROR, "could not initialize PLy_ResultType");
+}
+
+PyObject *
+PLy_result_new(void)
+{
+ PLyResultObject *ob;
+
+ if ((ob = PyObject_New(PLyResultObject, &PLy_ResultType)) == NULL)
+ return NULL;
+
+ /* ob->tuples = NULL; */
+
+ Py_INCREF(Py_None);
+ ob->status = Py_None;
+ ob->nrows = PyLong_FromLong(-1);
+ ob->rows = PyList_New(0);
+ ob->tupdesc = NULL;
+ if (!ob->rows)
+ {
+ Py_DECREF(ob);
+ return NULL;
+ }
+
+ return (PyObject *) ob;
+}
+
+static void
+PLy_result_dealloc(PyObject *arg)
+{
+ PLyResultObject *ob = (PLyResultObject *) arg;
+
+ Py_XDECREF(ob->nrows);
+ Py_XDECREF(ob->rows);
+ Py_XDECREF(ob->status);
+ if (ob->tupdesc)
+ {
+ FreeTupleDesc(ob->tupdesc);
+ ob->tupdesc = NULL;
+ }
+
+ arg->ob_type->tp_free(arg);
+}
+
+static PyObject *
+PLy_result_colnames(PyObject *self, PyObject *unused)
+{
+ PLyResultObject *ob = (PLyResultObject *) self;
+ PyObject *list;
+ int i;
+
+ if (!ob->tupdesc)
+ {
+ PLy_exception_set(PLy_exc_error, "command did not produce a result set");
+ return NULL;
+ }
+
+ list = PyList_New(ob->tupdesc->natts);
+ if (!list)
+ return NULL;
+ for (i = 0; i < ob->tupdesc->natts; i++)
+ {
+ Form_pg_attribute attr = TupleDescAttr(ob->tupdesc, i);
+
+ PyList_SET_ITEM(list, i, PLyUnicode_FromString(NameStr(attr->attname)));
+ }
+
+ return list;
+}
+
+static PyObject *
+PLy_result_coltypes(PyObject *self, PyObject *unused)
+{
+ PLyResultObject *ob = (PLyResultObject *) self;
+ PyObject *list;
+ int i;
+
+ if (!ob->tupdesc)
+ {
+ PLy_exception_set(PLy_exc_error, "command did not produce a result set");
+ return NULL;
+ }
+
+ list = PyList_New(ob->tupdesc->natts);
+ if (!list)
+ return NULL;
+ for (i = 0; i < ob->tupdesc->natts; i++)
+ {
+ Form_pg_attribute attr = TupleDescAttr(ob->tupdesc, i);
+
+ PyList_SET_ITEM(list, i, PyLong_FromLong(attr->atttypid));
+ }
+
+ return list;
+}
+
+static PyObject *
+PLy_result_coltypmods(PyObject *self, PyObject *unused)
+{
+ PLyResultObject *ob = (PLyResultObject *) self;
+ PyObject *list;
+ int i;
+
+ if (!ob->tupdesc)
+ {
+ PLy_exception_set(PLy_exc_error, "command did not produce a result set");
+ return NULL;
+ }
+
+ list = PyList_New(ob->tupdesc->natts);
+ if (!list)
+ return NULL;
+ for (i = 0; i < ob->tupdesc->natts; i++)
+ {
+ Form_pg_attribute attr = TupleDescAttr(ob->tupdesc, i);
+
+ PyList_SET_ITEM(list, i, PyLong_FromLong(attr->atttypmod));
+ }
+
+ return list;
+}
+
+static PyObject *
+PLy_result_nrows(PyObject *self, PyObject *args)
+{
+ PLyResultObject *ob = (PLyResultObject *) self;
+
+ Py_INCREF(ob->nrows);
+ return ob->nrows;
+}
+
+static PyObject *
+PLy_result_status(PyObject *self, PyObject *args)
+{
+ PLyResultObject *ob = (PLyResultObject *) self;
+
+ Py_INCREF(ob->status);
+ return ob->status;
+}
+
+static Py_ssize_t
+PLy_result_length(PyObject *arg)
+{
+ PLyResultObject *ob = (PLyResultObject *) arg;
+
+ return PyList_Size(ob->rows);
+}
+
+static PyObject *
+PLy_result_item(PyObject *arg, Py_ssize_t idx)
+{
+ PyObject *rv;
+ PLyResultObject *ob = (PLyResultObject *) arg;
+
+ rv = PyList_GetItem(ob->rows, idx);
+ if (rv != NULL)
+ Py_INCREF(rv);
+ return rv;
+}
+
+static PyObject *
+PLy_result_str(PyObject *arg)
+{
+ PLyResultObject *ob = (PLyResultObject *) arg;
+
+ return PyUnicode_FromFormat("<%s status=%S nrows=%S rows=%S>",
+ Py_TYPE(ob)->tp_name,
+ ob->status,
+ ob->nrows,
+ ob->rows);
+}
+
+static PyObject *
+PLy_result_subscript(PyObject *arg, PyObject *item)
+{
+ PLyResultObject *ob = (PLyResultObject *) arg;
+
+ return PyObject_GetItem(ob->rows, item);
+}
+
+static int
+PLy_result_ass_subscript(PyObject *arg, PyObject *item, PyObject *value)
+{
+ PLyResultObject *ob = (PLyResultObject *) arg;
+
+ return PyObject_SetItem(ob->rows, item, value);
+}
diff --git a/src/pl/plpython/plpy_resultobject.h b/src/pl/plpython/plpy_resultobject.h
new file mode 100644
index 0000000..978c4cc
--- /dev/null
+++ b/src/pl/plpython/plpy_resultobject.h
@@ -0,0 +1,27 @@
+/*
+ * src/pl/plpython/plpy_resultobject.h
+ */
+
+#ifndef PLPY_RESULTOBJECT_H
+#define PLPY_RESULTOBJECT_H
+
+#include "access/tupdesc.h"
+
+#include "plpython.h"
+
+
+typedef struct PLyResultObject
+{
+ PyObject_HEAD
+ /* HeapTuple *tuples; */
+ PyObject *nrows; /* number of rows returned by query */
+ PyObject *rows; /* data rows, or empty list if no data
+ * returned */
+ PyObject *status; /* query status, SPI_OK_*, or SPI_ERR_* */
+ TupleDesc tupdesc;
+} PLyResultObject;
+
+extern void PLy_result_init_type(void);
+extern PyObject *PLy_result_new(void);
+
+#endif /* PLPY_RESULTOBJECT_H */
diff --git a/src/pl/plpython/plpy_spi.c b/src/pl/plpython/plpy_spi.c
new file mode 100644
index 0000000..ff87b27
--- /dev/null
+++ b/src/pl/plpython/plpy_spi.c
@@ -0,0 +1,666 @@
+/*
+ * interface to SPI functions
+ *
+ * src/pl/plpython/plpy_spi.c
+ */
+
+#include "postgres.h"
+
+#include <limits.h>
+
+#include "access/htup_details.h"
+#include "access/xact.h"
+#include "catalog/pg_type.h"
+#include "executor/spi.h"
+#include "mb/pg_wchar.h"
+#include "parser/parse_type.h"
+#include "plpy_elog.h"
+#include "plpy_main.h"
+#include "plpy_planobject.h"
+#include "plpy_plpymodule.h"
+#include "plpy_procedure.h"
+#include "plpy_resultobject.h"
+#include "plpy_spi.h"
+#include "plpython.h"
+#include "utils/memutils.h"
+#include "utils/syscache.h"
+
+static PyObject *PLy_spi_execute_query(char *query, long limit);
+static PyObject *PLy_spi_execute_fetch_result(SPITupleTable *tuptable,
+ uint64 rows, int status);
+static void PLy_spi_exception_set(PyObject *excclass, ErrorData *edata);
+
+
+/* prepare(query="select * from foo")
+ * prepare(query="select * from foo where bar = $1", params=["text"])
+ * prepare(query="select * from foo where bar = $1", params=["text"], limit=5)
+ */
+PyObject *
+PLy_spi_prepare(PyObject *self, PyObject *args)
+{
+ PLyPlanObject *plan;
+ PyObject *list = NULL;
+ PyObject *volatile optr = NULL;
+ char *query;
+ PLyExecutionContext *exec_ctx = PLy_current_execution_context();
+ volatile MemoryContext oldcontext;
+ volatile ResourceOwner oldowner;
+ volatile int nargs;
+
+ if (!PyArg_ParseTuple(args, "s|O:prepare", &query, &list))
+ return NULL;
+
+ if (list && (!PySequence_Check(list)))
+ {
+ PLy_exception_set(PyExc_TypeError,
+ "second argument of plpy.prepare must be a sequence");
+ return NULL;
+ }
+
+ if ((plan = (PLyPlanObject *) PLy_plan_new()) == NULL)
+ return NULL;
+
+ plan->mcxt = AllocSetContextCreate(TopMemoryContext,
+ "PL/Python plan context",
+ ALLOCSET_DEFAULT_SIZES);
+ oldcontext = MemoryContextSwitchTo(plan->mcxt);
+
+ nargs = list ? PySequence_Length(list) : 0;
+
+ plan->nargs = nargs;
+ plan->types = nargs ? palloc0(sizeof(Oid) * nargs) : NULL;
+ plan->values = nargs ? palloc0(sizeof(Datum) * nargs) : NULL;
+ plan->args = nargs ? palloc0(sizeof(PLyObToDatum) * nargs) : NULL;
+
+ MemoryContextSwitchTo(oldcontext);
+
+ oldcontext = CurrentMemoryContext;
+ oldowner = CurrentResourceOwner;
+
+ PLy_spi_subtransaction_begin(oldcontext, oldowner);
+
+ PG_TRY();
+ {
+ int i;
+
+ for (i = 0; i < nargs; i++)
+ {
+ char *sptr;
+ Oid typeId;
+ int32 typmod;
+
+ optr = PySequence_GetItem(list, i);
+ if (PyUnicode_Check(optr))
+ sptr = PLyUnicode_AsString(optr);
+ else
+ {
+ ereport(ERROR,
+ (errmsg("plpy.prepare: type name at ordinal position %d is not a string", i)));
+ sptr = NULL; /* keep compiler quiet */
+ }
+
+ /********************************************************
+ * Resolve argument type names and then look them up by
+ * oid in the system cache, and remember the required
+ *information for input conversion.
+ ********************************************************/
+
+ (void) parseTypeString(sptr, &typeId, &typmod, NULL);
+
+ Py_DECREF(optr);
+
+ /*
+ * set optr to NULL, so we won't try to unref it again in case of
+ * an error
+ */
+ optr = NULL;
+
+ plan->types[i] = typeId;
+ PLy_output_setup_func(&plan->args[i], plan->mcxt,
+ typeId, typmod,
+ exec_ctx->curr_proc);
+ }
+
+ pg_verifymbstr(query, strlen(query), false);
+ plan->plan = SPI_prepare(query, plan->nargs, plan->types);
+ if (plan->plan == NULL)
+ elog(ERROR, "SPI_prepare failed: %s",
+ SPI_result_code_string(SPI_result));
+
+ /* transfer plan from procCxt to topCxt */
+ if (SPI_keepplan(plan->plan))
+ elog(ERROR, "SPI_keepplan failed");
+
+ PLy_spi_subtransaction_commit(oldcontext, oldowner);
+ }
+ PG_CATCH();
+ {
+ Py_DECREF(plan);
+ Py_XDECREF(optr);
+
+ PLy_spi_subtransaction_abort(oldcontext, oldowner);
+ return NULL;
+ }
+ PG_END_TRY();
+
+ Assert(plan->plan != NULL);
+ return (PyObject *) plan;
+}
+
+/* execute(query="select * from foo", limit=5)
+ * execute(plan=plan, values=(foo, bar), limit=5)
+ */
+PyObject *
+PLy_spi_execute(PyObject *self, PyObject *args)
+{
+ char *query;
+ PyObject *plan;
+ PyObject *list = NULL;
+ long limit = 0;
+
+ if (PyArg_ParseTuple(args, "s|l", &query, &limit))
+ return PLy_spi_execute_query(query, limit);
+
+ PyErr_Clear();
+
+ if (PyArg_ParseTuple(args, "O|Ol", &plan, &list, &limit) &&
+ is_PLyPlanObject(plan))
+ return PLy_spi_execute_plan(plan, list, limit);
+
+ PLy_exception_set(PLy_exc_error, "plpy.execute expected a query or a plan");
+ return NULL;
+}
+
+PyObject *
+PLy_spi_execute_plan(PyObject *ob, PyObject *list, long limit)
+{
+ volatile int nargs;
+ int i,
+ rv;
+ PLyPlanObject *plan;
+ volatile MemoryContext oldcontext;
+ volatile ResourceOwner oldowner;
+ PyObject *ret;
+
+ if (list != NULL)
+ {
+ if (!PySequence_Check(list) || PyUnicode_Check(list))
+ {
+ PLy_exception_set(PyExc_TypeError, "plpy.execute takes a sequence as its second argument");
+ return NULL;
+ }
+ nargs = PySequence_Length(list);
+ }
+ else
+ nargs = 0;
+
+ plan = (PLyPlanObject *) ob;
+
+ if (nargs != plan->nargs)
+ {
+ char *sv;
+ PyObject *so = PyObject_Str(list);
+
+ if (!so)
+ PLy_elog(ERROR, "could not execute plan");
+ sv = PLyUnicode_AsString(so);
+ PLy_exception_set_plural(PyExc_TypeError,
+ "Expected sequence of %d argument, got %d: %s",
+ "Expected sequence of %d arguments, got %d: %s",
+ plan->nargs,
+ plan->nargs, nargs, sv);
+ Py_DECREF(so);
+
+ return NULL;
+ }
+
+ oldcontext = CurrentMemoryContext;
+ oldowner = CurrentResourceOwner;
+
+ PLy_spi_subtransaction_begin(oldcontext, oldowner);
+
+ PG_TRY();
+ {
+ PLyExecutionContext *exec_ctx = PLy_current_execution_context();
+ char *volatile nulls;
+ volatile int j;
+
+ if (nargs > 0)
+ nulls = palloc(nargs * sizeof(char));
+ else
+ nulls = NULL;
+
+ for (j = 0; j < nargs; j++)
+ {
+ PLyObToDatum *arg = &plan->args[j];
+ PyObject *elem;
+
+ elem = PySequence_GetItem(list, j);
+ PG_TRY(2);
+ {
+ bool isnull;
+
+ plan->values[j] = PLy_output_convert(arg, elem, &isnull);
+ nulls[j] = isnull ? 'n' : ' ';
+ }
+ PG_FINALLY(2);
+ {
+ Py_DECREF(elem);
+ }
+ PG_END_TRY(2);
+ }
+
+ rv = SPI_execute_plan(plan->plan, plan->values, nulls,
+ exec_ctx->curr_proc->fn_readonly, limit);
+ ret = PLy_spi_execute_fetch_result(SPI_tuptable, SPI_processed, rv);
+
+ if (nargs > 0)
+ pfree(nulls);
+
+ PLy_spi_subtransaction_commit(oldcontext, oldowner);
+ }
+ PG_CATCH();
+ {
+ int k;
+
+ /*
+ * cleanup plan->values array
+ */
+ for (k = 0; k < nargs; k++)
+ {
+ if (!plan->args[k].typbyval &&
+ (plan->values[k] != PointerGetDatum(NULL)))
+ {
+ pfree(DatumGetPointer(plan->values[k]));
+ plan->values[k] = PointerGetDatum(NULL);
+ }
+ }
+
+ PLy_spi_subtransaction_abort(oldcontext, oldowner);
+ return NULL;
+ }
+ PG_END_TRY();
+
+ for (i = 0; i < nargs; i++)
+ {
+ if (!plan->args[i].typbyval &&
+ (plan->values[i] != PointerGetDatum(NULL)))
+ {
+ pfree(DatumGetPointer(plan->values[i]));
+ plan->values[i] = PointerGetDatum(NULL);
+ }
+ }
+
+ if (rv < 0)
+ {
+ PLy_exception_set(PLy_exc_spi_error,
+ "SPI_execute_plan failed: %s",
+ SPI_result_code_string(rv));
+ return NULL;
+ }
+
+ return ret;
+}
+
+static PyObject *
+PLy_spi_execute_query(char *query, long limit)
+{
+ int rv;
+ volatile MemoryContext oldcontext;
+ volatile ResourceOwner oldowner;
+ PyObject *ret = NULL;
+
+ oldcontext = CurrentMemoryContext;
+ oldowner = CurrentResourceOwner;
+
+ PLy_spi_subtransaction_begin(oldcontext, oldowner);
+
+ PG_TRY();
+ {
+ PLyExecutionContext *exec_ctx = PLy_current_execution_context();
+
+ pg_verifymbstr(query, strlen(query), false);
+ rv = SPI_execute(query, exec_ctx->curr_proc->fn_readonly, limit);
+ ret = PLy_spi_execute_fetch_result(SPI_tuptable, SPI_processed, rv);
+
+ PLy_spi_subtransaction_commit(oldcontext, oldowner);
+ }
+ PG_CATCH();
+ {
+ PLy_spi_subtransaction_abort(oldcontext, oldowner);
+ return NULL;
+ }
+ PG_END_TRY();
+
+ if (rv < 0)
+ {
+ Py_XDECREF(ret);
+ PLy_exception_set(PLy_exc_spi_error,
+ "SPI_execute failed: %s",
+ SPI_result_code_string(rv));
+ return NULL;
+ }
+
+ return ret;
+}
+
+static PyObject *
+PLy_spi_execute_fetch_result(SPITupleTable *tuptable, uint64 rows, int status)
+{
+ PLyResultObject *result;
+ PLyExecutionContext *exec_ctx = PLy_current_execution_context();
+ volatile MemoryContext oldcontext;
+
+ result = (PLyResultObject *) PLy_result_new();
+ if (!result)
+ {
+ SPI_freetuptable(tuptable);
+ return NULL;
+ }
+ Py_DECREF(result->status);
+ result->status = PyLong_FromLong(status);
+
+ if (status > 0 && tuptable == NULL)
+ {
+ Py_DECREF(result->nrows);
+ result->nrows = PyLong_FromUnsignedLongLong(rows);
+ }
+ else if (status > 0 && tuptable != NULL)
+ {
+ PLyDatumToOb ininfo;
+ MemoryContext cxt;
+
+ Py_DECREF(result->nrows);
+ result->nrows = PyLong_FromUnsignedLongLong(rows);
+
+ cxt = AllocSetContextCreate(CurrentMemoryContext,
+ "PL/Python temp context",
+ ALLOCSET_DEFAULT_SIZES);
+
+ /* Initialize for converting result tuples to Python */
+ PLy_input_setup_func(&ininfo, cxt, RECORDOID, -1,
+ exec_ctx->curr_proc);
+
+ oldcontext = CurrentMemoryContext;
+ PG_TRY();
+ {
+ MemoryContext oldcontext2;
+
+ if (rows)
+ {
+ uint64 i;
+
+ /*
+ * PyList_New() and PyList_SetItem() use Py_ssize_t for list
+ * size and list indices; so we cannot support a result larger
+ * than PY_SSIZE_T_MAX.
+ */
+ if (rows > (uint64) PY_SSIZE_T_MAX)
+ ereport(ERROR,
+ (errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED),
+ errmsg("query result has too many rows to fit in a Python list")));
+
+ Py_DECREF(result->rows);
+ result->rows = PyList_New(rows);
+ if (result->rows)
+ {
+ PLy_input_setup_tuple(&ininfo, tuptable->tupdesc,
+ exec_ctx->curr_proc);
+
+ for (i = 0; i < rows; i++)
+ {
+ PyObject *row = PLy_input_from_tuple(&ininfo,
+ tuptable->vals[i],
+ tuptable->tupdesc,
+ true);
+
+ PyList_SetItem(result->rows, i, row);
+ }
+ }
+ }
+
+ /*
+ * Save tuple descriptor for later use by result set metadata
+ * functions. Save it in TopMemoryContext so that it survives
+ * outside of an SPI context. We trust that PLy_result_dealloc()
+ * will clean it up when the time is right. (Do this as late as
+ * possible, to minimize the number of ways the tupdesc could get
+ * leaked due to errors.)
+ */
+ oldcontext2 = MemoryContextSwitchTo(TopMemoryContext);
+ result->tupdesc = CreateTupleDescCopy(tuptable->tupdesc);
+ MemoryContextSwitchTo(oldcontext2);
+ }
+ PG_CATCH();
+ {
+ MemoryContextSwitchTo(oldcontext);
+ MemoryContextDelete(cxt);
+ Py_DECREF(result);
+ PG_RE_THROW();
+ }
+ PG_END_TRY();
+
+ MemoryContextDelete(cxt);
+ SPI_freetuptable(tuptable);
+
+ /* in case PyList_New() failed above */
+ if (!result->rows)
+ {
+ Py_DECREF(result);
+ result = NULL;
+ }
+ }
+
+ return (PyObject *) result;
+}
+
+PyObject *
+PLy_commit(PyObject *self, PyObject *args)
+{
+ MemoryContext oldcontext = CurrentMemoryContext;
+ PLyExecutionContext *exec_ctx = PLy_current_execution_context();
+
+ PG_TRY();
+ {
+ SPI_commit();
+
+ /* was cleared at transaction end, reset pointer */
+ exec_ctx->scratch_ctx = NULL;
+ }
+ PG_CATCH();
+ {
+ ErrorData *edata;
+ PLyExceptionEntry *entry;
+ PyObject *exc;
+
+ /* Save error info */
+ MemoryContextSwitchTo(oldcontext);
+ edata = CopyErrorData();
+ FlushErrorState();
+
+ /* was cleared at transaction end, reset pointer */
+ exec_ctx->scratch_ctx = NULL;
+
+ /* Look up the correct exception */
+ entry = hash_search(PLy_spi_exceptions, &(edata->sqlerrcode),
+ HASH_FIND, NULL);
+
+ /*
+ * This could be a custom error code, if that's the case fallback to
+ * SPIError
+ */
+ exc = entry ? entry->exc : PLy_exc_spi_error;
+ /* Make Python raise the exception */
+ PLy_spi_exception_set(exc, edata);
+ FreeErrorData(edata);
+
+ return NULL;
+ }
+ PG_END_TRY();
+
+ Py_RETURN_NONE;
+}
+
+PyObject *
+PLy_rollback(PyObject *self, PyObject *args)
+{
+ MemoryContext oldcontext = CurrentMemoryContext;
+ PLyExecutionContext *exec_ctx = PLy_current_execution_context();
+
+ PG_TRY();
+ {
+ SPI_rollback();
+
+ /* was cleared at transaction end, reset pointer */
+ exec_ctx->scratch_ctx = NULL;
+ }
+ PG_CATCH();
+ {
+ ErrorData *edata;
+ PLyExceptionEntry *entry;
+ PyObject *exc;
+
+ /* Save error info */
+ MemoryContextSwitchTo(oldcontext);
+ edata = CopyErrorData();
+ FlushErrorState();
+
+ /* was cleared at transaction end, reset pointer */
+ exec_ctx->scratch_ctx = NULL;
+
+ /* Look up the correct exception */
+ entry = hash_search(PLy_spi_exceptions, &(edata->sqlerrcode),
+ HASH_FIND, NULL);
+
+ /*
+ * This could be a custom error code, if that's the case fallback to
+ * SPIError
+ */
+ exc = entry ? entry->exc : PLy_exc_spi_error;
+ /* Make Python raise the exception */
+ PLy_spi_exception_set(exc, edata);
+ FreeErrorData(edata);
+
+ return NULL;
+ }
+ PG_END_TRY();
+
+ Py_RETURN_NONE;
+}
+
+/*
+ * Utilities for running SPI functions in subtransactions.
+ *
+ * Usage:
+ *
+ * MemoryContext oldcontext = CurrentMemoryContext;
+ * ResourceOwner oldowner = CurrentResourceOwner;
+ *
+ * PLy_spi_subtransaction_begin(oldcontext, oldowner);
+ * PG_TRY();
+ * {
+ * <call SPI functions>
+ * PLy_spi_subtransaction_commit(oldcontext, oldowner);
+ * }
+ * PG_CATCH();
+ * {
+ * <do cleanup>
+ * PLy_spi_subtransaction_abort(oldcontext, oldowner);
+ * return NULL;
+ * }
+ * PG_END_TRY();
+ *
+ * These utilities take care of restoring connection to the SPI manager and
+ * setting a Python exception in case of an abort.
+ */
+void
+PLy_spi_subtransaction_begin(MemoryContext oldcontext, ResourceOwner oldowner)
+{
+ BeginInternalSubTransaction(NULL);
+ /* Want to run inside function's memory context */
+ MemoryContextSwitchTo(oldcontext);
+}
+
+void
+PLy_spi_subtransaction_commit(MemoryContext oldcontext, ResourceOwner oldowner)
+{
+ /* Commit the inner transaction, return to outer xact context */
+ ReleaseCurrentSubTransaction();
+ MemoryContextSwitchTo(oldcontext);
+ CurrentResourceOwner = oldowner;
+}
+
+void
+PLy_spi_subtransaction_abort(MemoryContext oldcontext, ResourceOwner oldowner)
+{
+ ErrorData *edata;
+ PLyExceptionEntry *entry;
+ PyObject *exc;
+
+ /* Save error info */
+ MemoryContextSwitchTo(oldcontext);
+ edata = CopyErrorData();
+ FlushErrorState();
+
+ /* Abort the inner transaction */
+ RollbackAndReleaseCurrentSubTransaction();
+ MemoryContextSwitchTo(oldcontext);
+ CurrentResourceOwner = oldowner;
+
+ /* Look up the correct exception */
+ entry = hash_search(PLy_spi_exceptions, &(edata->sqlerrcode),
+ HASH_FIND, NULL);
+
+ /*
+ * This could be a custom error code, if that's the case fallback to
+ * SPIError
+ */
+ exc = entry ? entry->exc : PLy_exc_spi_error;
+ /* Make Python raise the exception */
+ PLy_spi_exception_set(exc, edata);
+ FreeErrorData(edata);
+}
+
+/*
+ * Raise a SPIError, passing in it more error details, like the
+ * internal query and error position.
+ */
+static void
+PLy_spi_exception_set(PyObject *excclass, ErrorData *edata)
+{
+ PyObject *args = NULL;
+ PyObject *spierror = NULL;
+ PyObject *spidata = NULL;
+
+ args = Py_BuildValue("(s)", edata->message);
+ if (!args)
+ goto failure;
+
+ /* create a new SPI exception with the error message as the parameter */
+ spierror = PyObject_CallObject(excclass, args);
+ if (!spierror)
+ goto failure;
+
+ spidata = Py_BuildValue("(izzzizzzzz)", edata->sqlerrcode, edata->detail, edata->hint,
+ edata->internalquery, edata->internalpos,
+ edata->schema_name, edata->table_name, edata->column_name,
+ edata->datatype_name, edata->constraint_name);
+ if (!spidata)
+ goto failure;
+
+ if (PyObject_SetAttrString(spierror, "spidata", spidata) == -1)
+ goto failure;
+
+ PyErr_SetObject(excclass, spierror);
+
+ Py_DECREF(args);
+ Py_DECREF(spierror);
+ Py_DECREF(spidata);
+ return;
+
+failure:
+ Py_XDECREF(args);
+ Py_XDECREF(spierror);
+ Py_XDECREF(spidata);
+ elog(ERROR, "could not convert SPI error to Python exception");
+}
diff --git a/src/pl/plpython/plpy_spi.h b/src/pl/plpython/plpy_spi.h
new file mode 100644
index 0000000..98ccd21
--- /dev/null
+++ b/src/pl/plpython/plpy_spi.h
@@ -0,0 +1,29 @@
+/*
+ * src/pl/plpython/plpy_spi.h
+ */
+
+#ifndef PLPY_SPI_H
+#define PLPY_SPI_H
+
+#include "plpython.h"
+#include "utils/resowner.h"
+
+extern PyObject *PLy_spi_prepare(PyObject *self, PyObject *args);
+extern PyObject *PLy_spi_execute(PyObject *self, PyObject *args);
+extern PyObject *PLy_spi_execute_plan(PyObject *ob, PyObject *list, long limit);
+
+extern PyObject *PLy_commit(PyObject *self, PyObject *args);
+extern PyObject *PLy_rollback(PyObject *self, PyObject *args);
+
+typedef struct PLyExceptionEntry
+{
+ int sqlstate; /* hash key, must be first */
+ PyObject *exc; /* corresponding exception */
+} PLyExceptionEntry;
+
+/* handling of SPI operations inside subtransactions */
+extern void PLy_spi_subtransaction_begin(MemoryContext oldcontext, ResourceOwner oldowner);
+extern void PLy_spi_subtransaction_commit(MemoryContext oldcontext, ResourceOwner oldowner);
+extern void PLy_spi_subtransaction_abort(MemoryContext oldcontext, ResourceOwner oldowner);
+
+#endif /* PLPY_SPI_H */
diff --git a/src/pl/plpython/plpy_subxactobject.c b/src/pl/plpython/plpy_subxactobject.c
new file mode 100644
index 0000000..5c92a0e
--- /dev/null
+++ b/src/pl/plpython/plpy_subxactobject.c
@@ -0,0 +1,186 @@
+/*
+ * the PLySubtransaction class
+ *
+ * src/pl/plpython/plpy_subxactobject.c
+ */
+
+#include "postgres.h"
+
+#include "access/xact.h"
+#include "plpy_elog.h"
+#include "plpy_subxactobject.h"
+#include "plpython.h"
+#include "utils/memutils.h"
+
+List *explicit_subtransactions = NIL;
+
+
+static void PLy_subtransaction_dealloc(PyObject *subxact);
+static PyObject *PLy_subtransaction_enter(PyObject *self, PyObject *unused);
+static PyObject *PLy_subtransaction_exit(PyObject *self, PyObject *args);
+
+static char PLy_subtransaction_doc[] =
+"PostgreSQL subtransaction context manager";
+
+static PyMethodDef PLy_subtransaction_methods[] = {
+ {"__enter__", PLy_subtransaction_enter, METH_VARARGS, NULL},
+ {"__exit__", PLy_subtransaction_exit, METH_VARARGS, NULL},
+ /* user-friendly names for Python <2.6 */
+ {"enter", PLy_subtransaction_enter, METH_VARARGS, NULL},
+ {"exit", PLy_subtransaction_exit, METH_VARARGS, NULL},
+ {NULL, NULL, 0, NULL}
+};
+
+static PyTypeObject PLy_SubtransactionType = {
+ PyVarObject_HEAD_INIT(NULL, 0)
+ .tp_name = "PLySubtransaction",
+ .tp_basicsize = sizeof(PLySubtransactionObject),
+ .tp_dealloc = PLy_subtransaction_dealloc,
+ .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,
+ .tp_doc = PLy_subtransaction_doc,
+ .tp_methods = PLy_subtransaction_methods,
+};
+
+
+void
+PLy_subtransaction_init_type(void)
+{
+ if (PyType_Ready(&PLy_SubtransactionType) < 0)
+ elog(ERROR, "could not initialize PLy_SubtransactionType");
+}
+
+/* s = plpy.subtransaction() */
+PyObject *
+PLy_subtransaction_new(PyObject *self, PyObject *unused)
+{
+ PLySubtransactionObject *ob;
+
+ ob = PyObject_New(PLySubtransactionObject, &PLy_SubtransactionType);
+
+ if (ob == NULL)
+ return NULL;
+
+ ob->started = false;
+ ob->exited = false;
+
+ return (PyObject *) ob;
+}
+
+/* Python requires a dealloc function to be defined */
+static void
+PLy_subtransaction_dealloc(PyObject *subxact)
+{
+}
+
+/*
+ * subxact.__enter__() or subxact.enter()
+ *
+ * Start an explicit subtransaction. SPI calls within an explicit
+ * subtransaction will not start another one, so you can atomically
+ * execute many SPI calls and still get a controllable exception if
+ * one of them fails.
+ */
+static PyObject *
+PLy_subtransaction_enter(PyObject *self, PyObject *unused)
+{
+ PLySubtransactionData *subxactdata;
+ MemoryContext oldcontext;
+ PLySubtransactionObject *subxact = (PLySubtransactionObject *) self;
+
+ if (subxact->started)
+ {
+ PLy_exception_set(PyExc_ValueError, "this subtransaction has already been entered");
+ return NULL;
+ }
+
+ if (subxact->exited)
+ {
+ PLy_exception_set(PyExc_ValueError, "this subtransaction has already been exited");
+ return NULL;
+ }
+
+ subxact->started = true;
+ oldcontext = CurrentMemoryContext;
+
+ subxactdata = (PLySubtransactionData *)
+ MemoryContextAlloc(TopTransactionContext,
+ sizeof(PLySubtransactionData));
+
+ subxactdata->oldcontext = oldcontext;
+ subxactdata->oldowner = CurrentResourceOwner;
+
+ BeginInternalSubTransaction(NULL);
+
+ /* Be sure that cells of explicit_subtransactions list are long-lived */
+ MemoryContextSwitchTo(TopTransactionContext);
+ explicit_subtransactions = lcons(subxactdata, explicit_subtransactions);
+
+ /* Caller wants to stay in original memory context */
+ MemoryContextSwitchTo(oldcontext);
+
+ Py_INCREF(self);
+ return self;
+}
+
+/*
+ * subxact.__exit__(exc_type, exc, tb) or subxact.exit(exc_type, exc, tb)
+ *
+ * Exit an explicit subtransaction. exc_type is an exception type, exc
+ * is the exception object, tb is the traceback. If exc_type is None,
+ * commit the subtransaction, if not abort it.
+ *
+ * The method signature is chosen to allow subtransaction objects to
+ * be used as context managers as described in
+ * <http://www.python.org/dev/peps/pep-0343/>.
+ */
+static PyObject *
+PLy_subtransaction_exit(PyObject *self, PyObject *args)
+{
+ PyObject *type;
+ PyObject *value;
+ PyObject *traceback;
+ PLySubtransactionData *subxactdata;
+ PLySubtransactionObject *subxact = (PLySubtransactionObject *) self;
+
+ if (!PyArg_ParseTuple(args, "OOO", &type, &value, &traceback))
+ return NULL;
+
+ if (!subxact->started)
+ {
+ PLy_exception_set(PyExc_ValueError, "this subtransaction has not been entered");
+ return NULL;
+ }
+
+ if (subxact->exited)
+ {
+ PLy_exception_set(PyExc_ValueError, "this subtransaction has already been exited");
+ return NULL;
+ }
+
+ if (explicit_subtransactions == NIL)
+ {
+ PLy_exception_set(PyExc_ValueError, "there is no subtransaction to exit from");
+ return NULL;
+ }
+
+ subxact->exited = true;
+
+ if (type != Py_None)
+ {
+ /* Abort the inner transaction */
+ RollbackAndReleaseCurrentSubTransaction();
+ }
+ else
+ {
+ ReleaseCurrentSubTransaction();
+ }
+
+ subxactdata = (PLySubtransactionData *) linitial(explicit_subtransactions);
+ explicit_subtransactions = list_delete_first(explicit_subtransactions);
+
+ MemoryContextSwitchTo(subxactdata->oldcontext);
+ CurrentResourceOwner = subxactdata->oldowner;
+ pfree(subxactdata);
+
+ Py_RETURN_NONE;
+}
diff --git a/src/pl/plpython/plpy_subxactobject.h b/src/pl/plpython/plpy_subxactobject.h
new file mode 100644
index 0000000..5b6f863
--- /dev/null
+++ b/src/pl/plpython/plpy_subxactobject.h
@@ -0,0 +1,33 @@
+/*
+ * src/pl/plpython/plpy_subxactobject.h
+ */
+
+#ifndef PLPY_SUBXACTOBJECT
+#define PLPY_SUBXACTOBJECT
+
+#include "nodes/pg_list.h"
+#include "plpython.h"
+#include "utils/resowner.h"
+
+/* a list of nested explicit subtransactions */
+extern List *explicit_subtransactions;
+
+
+typedef struct PLySubtransactionObject
+{
+ PyObject_HEAD
+ bool started;
+ bool exited;
+} PLySubtransactionObject;
+
+/* explicit subtransaction data */
+typedef struct PLySubtransactionData
+{
+ MemoryContext oldcontext;
+ ResourceOwner oldowner;
+} PLySubtransactionData;
+
+extern void PLy_subtransaction_init_type(void);
+extern PyObject *PLy_subtransaction_new(PyObject *self, PyObject *unused);
+
+#endif /* PLPY_SUBXACTOBJECT */
diff --git a/src/pl/plpython/plpy_typeio.c b/src/pl/plpython/plpy_typeio.c
new file mode 100644
index 0000000..db14c5f
--- /dev/null
+++ b/src/pl/plpython/plpy_typeio.c
@@ -0,0 +1,1557 @@
+/*
+ * transforming Datums to Python objects and vice versa
+ *
+ * src/pl/plpython/plpy_typeio.c
+ */
+
+#include "postgres.h"
+
+#include "access/htup_details.h"
+#include "catalog/pg_type.h"
+#include "funcapi.h"
+#include "mb/pg_wchar.h"
+#include "miscadmin.h"
+#include "plpy_elog.h"
+#include "plpy_main.h"
+#include "plpy_typeio.h"
+#include "plpython.h"
+#include "utils/array.h"
+#include "utils/builtins.h"
+#include "utils/fmgroids.h"
+#include "utils/lsyscache.h"
+#include "utils/memutils.h"
+
+/* conversion from Datums to Python objects */
+static PyObject *PLyBool_FromBool(PLyDatumToOb *arg, Datum d);
+static PyObject *PLyFloat_FromFloat4(PLyDatumToOb *arg, Datum d);
+static PyObject *PLyFloat_FromFloat8(PLyDatumToOb *arg, Datum d);
+static PyObject *PLyDecimal_FromNumeric(PLyDatumToOb *arg, Datum d);
+static PyObject *PLyLong_FromInt16(PLyDatumToOb *arg, Datum d);
+static PyObject *PLyLong_FromInt32(PLyDatumToOb *arg, Datum d);
+static PyObject *PLyLong_FromInt64(PLyDatumToOb *arg, Datum d);
+static PyObject *PLyLong_FromOid(PLyDatumToOb *arg, Datum d);
+static PyObject *PLyBytes_FromBytea(PLyDatumToOb *arg, Datum d);
+static PyObject *PLyUnicode_FromScalar(PLyDatumToOb *arg, Datum d);
+static PyObject *PLyObject_FromTransform(PLyDatumToOb *arg, Datum d);
+static PyObject *PLyList_FromArray(PLyDatumToOb *arg, Datum d);
+static PyObject *PLyList_FromArray_recurse(PLyDatumToOb *elm, int *dims, int ndim, int dim,
+ char **dataptr_p, bits8 **bitmap_p, int *bitmask_p);
+static PyObject *PLyDict_FromComposite(PLyDatumToOb *arg, Datum d);
+static PyObject *PLyDict_FromTuple(PLyDatumToOb *arg, HeapTuple tuple, TupleDesc desc, bool include_generated);
+
+/* conversion from Python objects to Datums */
+static Datum PLyObject_ToBool(PLyObToDatum *arg, PyObject *plrv,
+ bool *isnull, bool inarray);
+static Datum PLyObject_ToBytea(PLyObToDatum *arg, PyObject *plrv,
+ bool *isnull, bool inarray);
+static Datum PLyObject_ToComposite(PLyObToDatum *arg, PyObject *plrv,
+ bool *isnull, bool inarray);
+static Datum PLyObject_ToScalar(PLyObToDatum *arg, PyObject *plrv,
+ bool *isnull, bool inarray);
+static Datum PLyObject_ToDomain(PLyObToDatum *arg, PyObject *plrv,
+ bool *isnull, bool inarray);
+static Datum PLyObject_ToTransform(PLyObToDatum *arg, PyObject *plrv,
+ bool *isnull, bool inarray);
+static Datum PLySequence_ToArray(PLyObToDatum *arg, PyObject *plrv,
+ bool *isnull, bool inarray);
+static void PLySequence_ToArray_recurse(PyObject *obj,
+ ArrayBuildState **astatep,
+ int *ndims, int *dims, int cur_depth,
+ PLyObToDatum *elm, Oid elmbasetype);
+
+/* conversion from Python objects to composite Datums */
+static Datum PLyUnicode_ToComposite(PLyObToDatum *arg, PyObject *string, bool inarray);
+static Datum PLyMapping_ToComposite(PLyObToDatum *arg, TupleDesc desc, PyObject *mapping);
+static Datum PLySequence_ToComposite(PLyObToDatum *arg, TupleDesc desc, PyObject *sequence);
+static Datum PLyGenericObject_ToComposite(PLyObToDatum *arg, TupleDesc desc, PyObject *object, bool inarray);
+
+
+/*
+ * Conversion functions. Remember output from Python is input to
+ * PostgreSQL, and vice versa.
+ */
+
+/*
+ * Perform input conversion, given correctly-set-up state information.
+ *
+ * This is the outer-level entry point for any input conversion. Internally,
+ * the conversion functions recurse directly to each other.
+ */
+PyObject *
+PLy_input_convert(PLyDatumToOb *arg, Datum val)
+{
+ PyObject *result;
+ PLyExecutionContext *exec_ctx = PLy_current_execution_context();
+ MemoryContext scratch_context = PLy_get_scratch_context(exec_ctx);
+ MemoryContext oldcontext;
+
+ /*
+ * Do the work in the scratch context to avoid leaking memory from the
+ * datatype output function calls. (The individual PLyDatumToObFunc
+ * functions can't reset the scratch context, because they recurse and an
+ * inner one might clobber data an outer one still needs. So we do it
+ * once at the outermost recursion level.)
+ *
+ * We reset the scratch context before, not after, each conversion cycle.
+ * This way we aren't on the hook to release a Python refcount on the
+ * result object in case MemoryContextReset throws an error.
+ */
+ MemoryContextReset(scratch_context);
+
+ oldcontext = MemoryContextSwitchTo(scratch_context);
+
+ result = arg->func(arg, val);
+
+ MemoryContextSwitchTo(oldcontext);
+
+ return result;
+}
+
+/*
+ * Perform output conversion, given correctly-set-up state information.
+ *
+ * This is the outer-level entry point for any output conversion. Internally,
+ * the conversion functions recurse directly to each other.
+ *
+ * The result, as well as any cruft generated along the way, are in the
+ * current memory context. Caller is responsible for cleanup.
+ */
+Datum
+PLy_output_convert(PLyObToDatum *arg, PyObject *val, bool *isnull)
+{
+ /* at outer level, we are not considering an array element */
+ return arg->func(arg, val, isnull, false);
+}
+
+/*
+ * Transform a tuple into a Python dict object.
+ *
+ * Note: the tupdesc must match the one used to set up *arg. We could
+ * insist that this function lookup the tupdesc from what is in *arg,
+ * but in practice all callers have the right tupdesc available.
+ */
+PyObject *
+PLy_input_from_tuple(PLyDatumToOb *arg, HeapTuple tuple, TupleDesc desc, bool include_generated)
+{
+ PyObject *dict;
+ PLyExecutionContext *exec_ctx = PLy_current_execution_context();
+ MemoryContext scratch_context = PLy_get_scratch_context(exec_ctx);
+ MemoryContext oldcontext;
+
+ /*
+ * As in PLy_input_convert, do the work in the scratch context.
+ */
+ MemoryContextReset(scratch_context);
+
+ oldcontext = MemoryContextSwitchTo(scratch_context);
+
+ dict = PLyDict_FromTuple(arg, tuple, desc, include_generated);
+
+ MemoryContextSwitchTo(oldcontext);
+
+ return dict;
+}
+
+/*
+ * Initialize, or re-initialize, per-column input info for a composite type.
+ *
+ * This is separate from PLy_input_setup_func() because in cases involving
+ * anonymous record types, we need to be passed the tupdesc explicitly.
+ * It's caller's responsibility that the tupdesc has adequate lifespan
+ * in such cases. If the tupdesc is for a named composite or registered
+ * record type, it does not need to be long-lived.
+ */
+void
+PLy_input_setup_tuple(PLyDatumToOb *arg, TupleDesc desc, PLyProcedure *proc)
+{
+ int i;
+
+ /* We should be working on a previously-set-up struct */
+ Assert(arg->func == PLyDict_FromComposite);
+
+ /* Save pointer to tupdesc, but only if this is an anonymous record type */
+ if (arg->typoid == RECORDOID && arg->typmod < 0)
+ arg->u.tuple.recdesc = desc;
+
+ /* (Re)allocate atts array as needed */
+ if (arg->u.tuple.natts != desc->natts)
+ {
+ if (arg->u.tuple.atts)
+ pfree(arg->u.tuple.atts);
+ arg->u.tuple.natts = desc->natts;
+ arg->u.tuple.atts = (PLyDatumToOb *)
+ MemoryContextAllocZero(arg->mcxt,
+ desc->natts * sizeof(PLyDatumToOb));
+ }
+
+ /* Fill the atts entries, except for dropped columns */
+ for (i = 0; i < desc->natts; i++)
+ {
+ Form_pg_attribute attr = TupleDescAttr(desc, i);
+ PLyDatumToOb *att = &arg->u.tuple.atts[i];
+
+ if (attr->attisdropped)
+ continue;
+
+ if (att->typoid == attr->atttypid && att->typmod == attr->atttypmod)
+ continue; /* already set up this entry */
+
+ PLy_input_setup_func(att, arg->mcxt,
+ attr->atttypid, attr->atttypmod,
+ proc);
+ }
+}
+
+/*
+ * Initialize, or re-initialize, per-column output info for a composite type.
+ *
+ * This is separate from PLy_output_setup_func() because in cases involving
+ * anonymous record types, we need to be passed the tupdesc explicitly.
+ * It's caller's responsibility that the tupdesc has adequate lifespan
+ * in such cases. If the tupdesc is for a named composite or registered
+ * record type, it does not need to be long-lived.
+ */
+void
+PLy_output_setup_tuple(PLyObToDatum *arg, TupleDesc desc, PLyProcedure *proc)
+{
+ int i;
+
+ /* We should be working on a previously-set-up struct */
+ Assert(arg->func == PLyObject_ToComposite);
+
+ /* Save pointer to tupdesc, but only if this is an anonymous record type */
+ if (arg->typoid == RECORDOID && arg->typmod < 0)
+ arg->u.tuple.recdesc = desc;
+
+ /* (Re)allocate atts array as needed */
+ if (arg->u.tuple.natts != desc->natts)
+ {
+ if (arg->u.tuple.atts)
+ pfree(arg->u.tuple.atts);
+ arg->u.tuple.natts = desc->natts;
+ arg->u.tuple.atts = (PLyObToDatum *)
+ MemoryContextAllocZero(arg->mcxt,
+ desc->natts * sizeof(PLyObToDatum));
+ }
+
+ /* Fill the atts entries, except for dropped columns */
+ for (i = 0; i < desc->natts; i++)
+ {
+ Form_pg_attribute attr = TupleDescAttr(desc, i);
+ PLyObToDatum *att = &arg->u.tuple.atts[i];
+
+ if (attr->attisdropped)
+ continue;
+
+ if (att->typoid == attr->atttypid && att->typmod == attr->atttypmod)
+ continue; /* already set up this entry */
+
+ PLy_output_setup_func(att, arg->mcxt,
+ attr->atttypid, attr->atttypmod,
+ proc);
+ }
+}
+
+/*
+ * Set up output info for a PL/Python function returning record.
+ *
+ * Note: the given tupdesc is not necessarily long-lived.
+ */
+void
+PLy_output_setup_record(PLyObToDatum *arg, TupleDesc desc, PLyProcedure *proc)
+{
+ /* Makes no sense unless RECORD */
+ Assert(arg->typoid == RECORDOID);
+ Assert(desc->tdtypeid == RECORDOID);
+
+ /*
+ * Bless the record type if not already done. We'd have to do this anyway
+ * to return a tuple, so we might as well force the issue so we can use
+ * the known-record-type code path.
+ */
+ BlessTupleDesc(desc);
+
+ /*
+ * Update arg->typmod, and clear the recdesc link if it's changed. The
+ * next call of PLyObject_ToComposite will look up a long-lived tupdesc
+ * for the record type.
+ */
+ arg->typmod = desc->tdtypmod;
+ if (arg->u.tuple.recdesc &&
+ arg->u.tuple.recdesc->tdtypmod != arg->typmod)
+ arg->u.tuple.recdesc = NULL;
+
+ /* Update derived data if necessary */
+ PLy_output_setup_tuple(arg, desc, proc);
+}
+
+/*
+ * Recursively initialize the PLyObToDatum structure(s) needed to construct
+ * a SQL value of the specified typeOid/typmod from a Python value.
+ * (But note that at this point we may have RECORDOID/-1, ie, an indeterminate
+ * record type.)
+ * proc is used to look up transform functions.
+ */
+void
+PLy_output_setup_func(PLyObToDatum *arg, MemoryContext arg_mcxt,
+ Oid typeOid, int32 typmod,
+ PLyProcedure *proc)
+{
+ TypeCacheEntry *typentry;
+ char typtype;
+ Oid trfuncid;
+ Oid typinput;
+
+ /* Since this is recursive, it could theoretically be driven to overflow */
+ check_stack_depth();
+
+ arg->typoid = typeOid;
+ arg->typmod = typmod;
+ arg->mcxt = arg_mcxt;
+
+ /*
+ * Fetch typcache entry for the target type, asking for whatever info
+ * we'll need later. RECORD is a special case: just treat it as composite
+ * without bothering with the typcache entry.
+ */
+ if (typeOid != RECORDOID)
+ {
+ typentry = lookup_type_cache(typeOid, TYPECACHE_DOMAIN_BASE_INFO);
+ typtype = typentry->typtype;
+ arg->typbyval = typentry->typbyval;
+ arg->typlen = typentry->typlen;
+ arg->typalign = typentry->typalign;
+ }
+ else
+ {
+ typentry = NULL;
+ typtype = TYPTYPE_COMPOSITE;
+ /* hard-wired knowledge about type RECORD: */
+ arg->typbyval = false;
+ arg->typlen = -1;
+ arg->typalign = TYPALIGN_DOUBLE;
+ }
+
+ /*
+ * Choose conversion method. Note that transform functions are checked
+ * for composite and scalar types, but not for arrays or domains. This is
+ * somewhat historical, but we'd have a problem allowing them on domains,
+ * since we drill down through all levels of a domain nest without looking
+ * at the intermediate levels at all.
+ */
+ if (typtype == TYPTYPE_DOMAIN)
+ {
+ /* Domain */
+ arg->func = PLyObject_ToDomain;
+ arg->u.domain.domain_info = NULL;
+ /* Recursively set up conversion info for the element type */
+ arg->u.domain.base = (PLyObToDatum *)
+ MemoryContextAllocZero(arg_mcxt, sizeof(PLyObToDatum));
+ PLy_output_setup_func(arg->u.domain.base, arg_mcxt,
+ typentry->domainBaseType,
+ typentry->domainBaseTypmod,
+ proc);
+ }
+ else if (typentry &&
+ IsTrueArrayType(typentry))
+ {
+ /* Standard array */
+ arg->func = PLySequence_ToArray;
+ /* Get base type OID to insert into constructed array */
+ /* (note this might not be the same as the immediate child type) */
+ arg->u.array.elmbasetype = getBaseType(typentry->typelem);
+ /* Recursively set up conversion info for the element type */
+ arg->u.array.elm = (PLyObToDatum *)
+ MemoryContextAllocZero(arg_mcxt, sizeof(PLyObToDatum));
+ PLy_output_setup_func(arg->u.array.elm, arg_mcxt,
+ typentry->typelem, typmod,
+ proc);
+ }
+ else if ((trfuncid = get_transform_tosql(typeOid,
+ proc->langid,
+ proc->trftypes)))
+ {
+ arg->func = PLyObject_ToTransform;
+ fmgr_info_cxt(trfuncid, &arg->u.transform.typtransform, arg_mcxt);
+ }
+ else if (typtype == TYPTYPE_COMPOSITE)
+ {
+ /* Named composite type, or RECORD */
+ arg->func = PLyObject_ToComposite;
+ /* We'll set up the per-field data later */
+ arg->u.tuple.recdesc = NULL;
+ arg->u.tuple.typentry = typentry;
+ arg->u.tuple.tupdescid = INVALID_TUPLEDESC_IDENTIFIER;
+ arg->u.tuple.atts = NULL;
+ arg->u.tuple.natts = 0;
+ /* Mark this invalid till needed, too */
+ arg->u.tuple.recinfunc.fn_oid = InvalidOid;
+ }
+ else
+ {
+ /* Scalar type, but we have a couple of special cases */
+ switch (typeOid)
+ {
+ case BOOLOID:
+ arg->func = PLyObject_ToBool;
+ break;
+ case BYTEAOID:
+ arg->func = PLyObject_ToBytea;
+ break;
+ default:
+ arg->func = PLyObject_ToScalar;
+ getTypeInputInfo(typeOid, &typinput, &arg->u.scalar.typioparam);
+ fmgr_info_cxt(typinput, &arg->u.scalar.typfunc, arg_mcxt);
+ break;
+ }
+ }
+}
+
+/*
+ * Recursively initialize the PLyDatumToOb structure(s) needed to construct
+ * a Python value from a SQL value of the specified typeOid/typmod.
+ * (But note that at this point we may have RECORDOID/-1, ie, an indeterminate
+ * record type.)
+ * proc is used to look up transform functions.
+ */
+void
+PLy_input_setup_func(PLyDatumToOb *arg, MemoryContext arg_mcxt,
+ Oid typeOid, int32 typmod,
+ PLyProcedure *proc)
+{
+ TypeCacheEntry *typentry;
+ char typtype;
+ Oid trfuncid;
+ Oid typoutput;
+ bool typisvarlena;
+
+ /* Since this is recursive, it could theoretically be driven to overflow */
+ check_stack_depth();
+
+ arg->typoid = typeOid;
+ arg->typmod = typmod;
+ arg->mcxt = arg_mcxt;
+
+ /*
+ * Fetch typcache entry for the target type, asking for whatever info
+ * we'll need later. RECORD is a special case: just treat it as composite
+ * without bothering with the typcache entry.
+ */
+ if (typeOid != RECORDOID)
+ {
+ typentry = lookup_type_cache(typeOid, TYPECACHE_DOMAIN_BASE_INFO);
+ typtype = typentry->typtype;
+ arg->typbyval = typentry->typbyval;
+ arg->typlen = typentry->typlen;
+ arg->typalign = typentry->typalign;
+ }
+ else
+ {
+ typentry = NULL;
+ typtype = TYPTYPE_COMPOSITE;
+ /* hard-wired knowledge about type RECORD: */
+ arg->typbyval = false;
+ arg->typlen = -1;
+ arg->typalign = TYPALIGN_DOUBLE;
+ }
+
+ /*
+ * Choose conversion method. Note that transform functions are checked
+ * for composite and scalar types, but not for arrays or domains. This is
+ * somewhat historical, but we'd have a problem allowing them on domains,
+ * since we drill down through all levels of a domain nest without looking
+ * at the intermediate levels at all.
+ */
+ if (typtype == TYPTYPE_DOMAIN)
+ {
+ /* Domain --- we don't care, just recurse down to the base type */
+ PLy_input_setup_func(arg, arg_mcxt,
+ typentry->domainBaseType,
+ typentry->domainBaseTypmod,
+ proc);
+ }
+ else if (typentry &&
+ IsTrueArrayType(typentry))
+ {
+ /* Standard array */
+ arg->func = PLyList_FromArray;
+ /* Recursively set up conversion info for the element type */
+ arg->u.array.elm = (PLyDatumToOb *)
+ MemoryContextAllocZero(arg_mcxt, sizeof(PLyDatumToOb));
+ PLy_input_setup_func(arg->u.array.elm, arg_mcxt,
+ typentry->typelem, typmod,
+ proc);
+ }
+ else if ((trfuncid = get_transform_fromsql(typeOid,
+ proc->langid,
+ proc->trftypes)))
+ {
+ arg->func = PLyObject_FromTransform;
+ fmgr_info_cxt(trfuncid, &arg->u.transform.typtransform, arg_mcxt);
+ }
+ else if (typtype == TYPTYPE_COMPOSITE)
+ {
+ /* Named composite type, or RECORD */
+ arg->func = PLyDict_FromComposite;
+ /* We'll set up the per-field data later */
+ arg->u.tuple.recdesc = NULL;
+ arg->u.tuple.typentry = typentry;
+ arg->u.tuple.tupdescid = INVALID_TUPLEDESC_IDENTIFIER;
+ arg->u.tuple.atts = NULL;
+ arg->u.tuple.natts = 0;
+ }
+ else
+ {
+ /* Scalar type, but we have a couple of special cases */
+ switch (typeOid)
+ {
+ case BOOLOID:
+ arg->func = PLyBool_FromBool;
+ break;
+ case FLOAT4OID:
+ arg->func = PLyFloat_FromFloat4;
+ break;
+ case FLOAT8OID:
+ arg->func = PLyFloat_FromFloat8;
+ break;
+ case NUMERICOID:
+ arg->func = PLyDecimal_FromNumeric;
+ break;
+ case INT2OID:
+ arg->func = PLyLong_FromInt16;
+ break;
+ case INT4OID:
+ arg->func = PLyLong_FromInt32;
+ break;
+ case INT8OID:
+ arg->func = PLyLong_FromInt64;
+ break;
+ case OIDOID:
+ arg->func = PLyLong_FromOid;
+ break;
+ case BYTEAOID:
+ arg->func = PLyBytes_FromBytea;
+ break;
+ default:
+ arg->func = PLyUnicode_FromScalar;
+ getTypeOutputInfo(typeOid, &typoutput, &typisvarlena);
+ fmgr_info_cxt(typoutput, &arg->u.scalar.typfunc, arg_mcxt);
+ break;
+ }
+ }
+}
+
+
+/*
+ * Special-purpose input converters.
+ */
+
+static PyObject *
+PLyBool_FromBool(PLyDatumToOb *arg, Datum d)
+{
+ if (DatumGetBool(d))
+ Py_RETURN_TRUE;
+ Py_RETURN_FALSE;
+}
+
+static PyObject *
+PLyFloat_FromFloat4(PLyDatumToOb *arg, Datum d)
+{
+ return PyFloat_FromDouble(DatumGetFloat4(d));
+}
+
+static PyObject *
+PLyFloat_FromFloat8(PLyDatumToOb *arg, Datum d)
+{
+ return PyFloat_FromDouble(DatumGetFloat8(d));
+}
+
+static PyObject *
+PLyDecimal_FromNumeric(PLyDatumToOb *arg, Datum d)
+{
+ static PyObject *decimal_constructor;
+ char *str;
+ PyObject *pyvalue;
+
+ /* Try to import cdecimal. If it doesn't exist, fall back to decimal. */
+ if (!decimal_constructor)
+ {
+ PyObject *decimal_module;
+
+ decimal_module = PyImport_ImportModule("cdecimal");
+ if (!decimal_module)
+ {
+ PyErr_Clear();
+ decimal_module = PyImport_ImportModule("decimal");
+ }
+ if (!decimal_module)
+ PLy_elog(ERROR, "could not import a module for Decimal constructor");
+
+ decimal_constructor = PyObject_GetAttrString(decimal_module, "Decimal");
+ if (!decimal_constructor)
+ PLy_elog(ERROR, "no Decimal attribute in module");
+ }
+
+ str = DatumGetCString(DirectFunctionCall1(numeric_out, d));
+ pyvalue = PyObject_CallFunction(decimal_constructor, "s", str);
+ if (!pyvalue)
+ PLy_elog(ERROR, "conversion from numeric to Decimal failed");
+
+ return pyvalue;
+}
+
+static PyObject *
+PLyLong_FromInt16(PLyDatumToOb *arg, Datum d)
+{
+ return PyLong_FromLong(DatumGetInt16(d));
+}
+
+static PyObject *
+PLyLong_FromInt32(PLyDatumToOb *arg, Datum d)
+{
+ return PyLong_FromLong(DatumGetInt32(d));
+}
+
+static PyObject *
+PLyLong_FromInt64(PLyDatumToOb *arg, Datum d)
+{
+ return PyLong_FromLongLong(DatumGetInt64(d));
+}
+
+static PyObject *
+PLyLong_FromOid(PLyDatumToOb *arg, Datum d)
+{
+ return PyLong_FromUnsignedLong(DatumGetObjectId(d));
+}
+
+static PyObject *
+PLyBytes_FromBytea(PLyDatumToOb *arg, Datum d)
+{
+ text *txt = DatumGetByteaPP(d);
+ char *str = VARDATA_ANY(txt);
+ size_t size = VARSIZE_ANY_EXHDR(txt);
+
+ return PyBytes_FromStringAndSize(str, size);
+}
+
+
+/*
+ * Generic input conversion using a SQL type's output function.
+ */
+static PyObject *
+PLyUnicode_FromScalar(PLyDatumToOb *arg, Datum d)
+{
+ char *x = OutputFunctionCall(&arg->u.scalar.typfunc, d);
+ PyObject *r = PLyUnicode_FromString(x);
+
+ pfree(x);
+ return r;
+}
+
+/*
+ * Convert using a from-SQL transform function.
+ */
+static PyObject *
+PLyObject_FromTransform(PLyDatumToOb *arg, Datum d)
+{
+ Datum t;
+
+ t = FunctionCall1(&arg->u.transform.typtransform, d);
+ return (PyObject *) DatumGetPointer(t);
+}
+
+/*
+ * Convert a SQL array to a Python list.
+ */
+static PyObject *
+PLyList_FromArray(PLyDatumToOb *arg, Datum d)
+{
+ ArrayType *array = DatumGetArrayTypeP(d);
+ PLyDatumToOb *elm = arg->u.array.elm;
+ int ndim;
+ int *dims;
+ char *dataptr;
+ bits8 *bitmap;
+ int bitmask;
+
+ if (ARR_NDIM(array) == 0)
+ return PyList_New(0);
+
+ /* Array dimensions and left bounds */
+ ndim = ARR_NDIM(array);
+ dims = ARR_DIMS(array);
+ Assert(ndim <= MAXDIM);
+
+ /*
+ * We iterate the SQL array in the physical order it's stored in the
+ * datum. For example, for a 3-dimensional array the order of iteration
+ * would be the following: [0,0,0] elements through [0,0,k], then [0,1,0]
+ * through [0,1,k] till [0,m,k], then [1,0,0] through [1,0,k] till
+ * [1,m,k], and so on.
+ *
+ * In Python, there are no multi-dimensional lists as such, but they are
+ * represented as a list of lists. So a 3-d array of [n,m,k] elements is a
+ * list of n m-element arrays, each element of which is k-element array.
+ * PLyList_FromArray_recurse() builds the Python list for a single
+ * dimension, and recurses for the next inner dimension.
+ */
+ dataptr = ARR_DATA_PTR(array);
+ bitmap = ARR_NULLBITMAP(array);
+ bitmask = 1;
+
+ return PLyList_FromArray_recurse(elm, dims, ndim, 0,
+ &dataptr, &bitmap, &bitmask);
+}
+
+static PyObject *
+PLyList_FromArray_recurse(PLyDatumToOb *elm, int *dims, int ndim, int dim,
+ char **dataptr_p, bits8 **bitmap_p, int *bitmask_p)
+{
+ int i;
+ PyObject *list;
+
+ list = PyList_New(dims[dim]);
+ if (!list)
+ return NULL;
+
+ if (dim < ndim - 1)
+ {
+ /* Outer dimension. Recurse for each inner slice. */
+ for (i = 0; i < dims[dim]; i++)
+ {
+ PyObject *sublist;
+
+ sublist = PLyList_FromArray_recurse(elm, dims, ndim, dim + 1,
+ dataptr_p, bitmap_p, bitmask_p);
+ PyList_SET_ITEM(list, i, sublist);
+ }
+ }
+ else
+ {
+ /*
+ * Innermost dimension. Fill the list with the values from the array
+ * for this slice.
+ */
+ char *dataptr = *dataptr_p;
+ bits8 *bitmap = *bitmap_p;
+ int bitmask = *bitmask_p;
+
+ for (i = 0; i < dims[dim]; i++)
+ {
+ /* checking for NULL */
+ if (bitmap && (*bitmap & bitmask) == 0)
+ {
+ Py_INCREF(Py_None);
+ PyList_SET_ITEM(list, i, Py_None);
+ }
+ else
+ {
+ Datum itemvalue;
+
+ itemvalue = fetch_att(dataptr, elm->typbyval, elm->typlen);
+ PyList_SET_ITEM(list, i, elm->func(elm, itemvalue));
+ dataptr = att_addlength_pointer(dataptr, elm->typlen, dataptr);
+ dataptr = (char *) att_align_nominal(dataptr, elm->typalign);
+ }
+
+ /* advance bitmap pointer if any */
+ if (bitmap)
+ {
+ bitmask <<= 1;
+ if (bitmask == 0x100 /* (1<<8) */ )
+ {
+ bitmap++;
+ bitmask = 1;
+ }
+ }
+ }
+
+ *dataptr_p = dataptr;
+ *bitmap_p = bitmap;
+ *bitmask_p = bitmask;
+ }
+
+ return list;
+}
+
+/*
+ * Convert a composite SQL value to a Python dict.
+ */
+static PyObject *
+PLyDict_FromComposite(PLyDatumToOb *arg, Datum d)
+{
+ PyObject *dict;
+ HeapTupleHeader td;
+ Oid tupType;
+ int32 tupTypmod;
+ TupleDesc tupdesc;
+ HeapTupleData tmptup;
+
+ td = DatumGetHeapTupleHeader(d);
+ /* Extract rowtype info and find a tupdesc */
+ tupType = HeapTupleHeaderGetTypeId(td);
+ tupTypmod = HeapTupleHeaderGetTypMod(td);
+ tupdesc = lookup_rowtype_tupdesc(tupType, tupTypmod);
+
+ /* Set up I/O funcs if not done yet */
+ PLy_input_setup_tuple(arg, tupdesc,
+ PLy_current_execution_context()->curr_proc);
+
+ /* Build a temporary HeapTuple control structure */
+ tmptup.t_len = HeapTupleHeaderGetDatumLength(td);
+ tmptup.t_data = td;
+
+ dict = PLyDict_FromTuple(arg, &tmptup, tupdesc, true);
+
+ ReleaseTupleDesc(tupdesc);
+
+ return dict;
+}
+
+/*
+ * Transform a tuple into a Python dict object.
+ */
+static PyObject *
+PLyDict_FromTuple(PLyDatumToOb *arg, HeapTuple tuple, TupleDesc desc, bool include_generated)
+{
+ PyObject *volatile dict;
+
+ /* Simple sanity check that desc matches */
+ Assert(desc->natts == arg->u.tuple.natts);
+
+ dict = PyDict_New();
+ if (dict == NULL)
+ return NULL;
+
+ PG_TRY();
+ {
+ int i;
+
+ for (i = 0; i < arg->u.tuple.natts; i++)
+ {
+ PLyDatumToOb *att = &arg->u.tuple.atts[i];
+ Form_pg_attribute attr = TupleDescAttr(desc, i);
+ char *key;
+ Datum vattr;
+ bool is_null;
+ PyObject *value;
+
+ if (attr->attisdropped)
+ continue;
+
+ if (attr->attgenerated)
+ {
+ /* don't include unless requested */
+ if (!include_generated)
+ continue;
+ }
+
+ key = NameStr(attr->attname);
+ vattr = heap_getattr(tuple, (i + 1), desc, &is_null);
+
+ if (is_null)
+ PyDict_SetItemString(dict, key, Py_None);
+ else
+ {
+ value = att->func(att, vattr);
+ PyDict_SetItemString(dict, key, value);
+ Py_DECREF(value);
+ }
+ }
+ }
+ PG_CATCH();
+ {
+ Py_DECREF(dict);
+ PG_RE_THROW();
+ }
+ PG_END_TRY();
+
+ return dict;
+}
+
+/*
+ * Convert a Python object to a PostgreSQL bool datum. This can't go
+ * through the generic conversion function, because Python attaches a
+ * Boolean value to everything, more things than the PostgreSQL bool
+ * type can parse.
+ */
+static Datum
+PLyObject_ToBool(PLyObToDatum *arg, PyObject *plrv,
+ bool *isnull, bool inarray)
+{
+ if (plrv == Py_None)
+ {
+ *isnull = true;
+ return (Datum) 0;
+ }
+ *isnull = false;
+ return BoolGetDatum(PyObject_IsTrue(plrv));
+}
+
+/*
+ * Convert a Python object to a PostgreSQL bytea datum. This doesn't
+ * go through the generic conversion function to circumvent problems
+ * with embedded nulls. And it's faster this way.
+ */
+static Datum
+PLyObject_ToBytea(PLyObToDatum *arg, PyObject *plrv,
+ bool *isnull, bool inarray)
+{
+ PyObject *volatile plrv_so = NULL;
+ Datum rv = (Datum) 0;
+
+ if (plrv == Py_None)
+ {
+ *isnull = true;
+ return (Datum) 0;
+ }
+ *isnull = false;
+
+ plrv_so = PyObject_Bytes(plrv);
+ if (!plrv_so)
+ PLy_elog(ERROR, "could not create bytes representation of Python object");
+
+ PG_TRY();
+ {
+ char *plrv_sc = PyBytes_AsString(plrv_so);
+ size_t len = PyBytes_Size(plrv_so);
+ size_t size = len + VARHDRSZ;
+ bytea *result = palloc(size);
+
+ SET_VARSIZE(result, size);
+ memcpy(VARDATA(result), plrv_sc, len);
+ rv = PointerGetDatum(result);
+ }
+ PG_FINALLY();
+ {
+ Py_XDECREF(plrv_so);
+ }
+ PG_END_TRY();
+
+ return rv;
+}
+
+
+/*
+ * Convert a Python object to a composite type. First look up the type's
+ * description, then route the Python object through the conversion function
+ * for obtaining PostgreSQL tuples.
+ */
+static Datum
+PLyObject_ToComposite(PLyObToDatum *arg, PyObject *plrv,
+ bool *isnull, bool inarray)
+{
+ Datum rv;
+ TupleDesc desc;
+
+ if (plrv == Py_None)
+ {
+ *isnull = true;
+ return (Datum) 0;
+ }
+ *isnull = false;
+
+ /*
+ * The string conversion case doesn't require a tupdesc, nor per-field
+ * conversion data, so just go for it if that's the case to use.
+ */
+ if (PyUnicode_Check(plrv))
+ return PLyUnicode_ToComposite(arg, plrv, inarray);
+
+ /*
+ * If we're dealing with a named composite type, we must look up the
+ * tupdesc every time, to protect against possible changes to the type.
+ * RECORD types can't change between calls; but we must still be willing
+ * to set up the info the first time, if nobody did yet.
+ */
+ if (arg->typoid != RECORDOID)
+ {
+ desc = lookup_rowtype_tupdesc(arg->typoid, arg->typmod);
+ /* We should have the descriptor of the type's typcache entry */
+ Assert(desc == arg->u.tuple.typentry->tupDesc);
+ /* Detect change of descriptor, update cache if needed */
+ if (arg->u.tuple.tupdescid != arg->u.tuple.typentry->tupDesc_identifier)
+ {
+ PLy_output_setup_tuple(arg, desc,
+ PLy_current_execution_context()->curr_proc);
+ arg->u.tuple.tupdescid = arg->u.tuple.typentry->tupDesc_identifier;
+ }
+ }
+ else
+ {
+ desc = arg->u.tuple.recdesc;
+ if (desc == NULL)
+ {
+ desc = lookup_rowtype_tupdesc(arg->typoid, arg->typmod);
+ arg->u.tuple.recdesc = desc;
+ }
+ else
+ {
+ /* Pin descriptor to match unpin below */
+ PinTupleDesc(desc);
+ }
+ }
+
+ /* Simple sanity check on our caching */
+ Assert(desc->natts == arg->u.tuple.natts);
+
+ /*
+ * Convert, using the appropriate method depending on the type of the
+ * supplied Python object.
+ */
+ if (PySequence_Check(plrv))
+ /* composite type as sequence (tuple, list etc) */
+ rv = PLySequence_ToComposite(arg, desc, plrv);
+ else if (PyMapping_Check(plrv))
+ /* composite type as mapping (currently only dict) */
+ rv = PLyMapping_ToComposite(arg, desc, plrv);
+ else
+ /* returned as smth, must provide method __getattr__(name) */
+ rv = PLyGenericObject_ToComposite(arg, desc, plrv, inarray);
+
+ ReleaseTupleDesc(desc);
+
+ return rv;
+}
+
+
+/*
+ * Convert Python object to C string in server encoding.
+ *
+ * Note: this is exported for use by add-on transform modules.
+ */
+char *
+PLyObject_AsString(PyObject *plrv)
+{
+ PyObject *plrv_bo;
+ char *plrv_sc;
+ size_t plen;
+ size_t slen;
+
+ if (PyUnicode_Check(plrv))
+ plrv_bo = PLyUnicode_Bytes(plrv);
+ else if (PyFloat_Check(plrv))
+ {
+ /* use repr() for floats, str() is lossy */
+ PyObject *s = PyObject_Repr(plrv);
+
+ plrv_bo = PLyUnicode_Bytes(s);
+ Py_XDECREF(s);
+ }
+ else
+ {
+ PyObject *s = PyObject_Str(plrv);
+
+ plrv_bo = PLyUnicode_Bytes(s);
+ Py_XDECREF(s);
+ }
+ if (!plrv_bo)
+ PLy_elog(ERROR, "could not create string representation of Python object");
+
+ plrv_sc = pstrdup(PyBytes_AsString(plrv_bo));
+ plen = PyBytes_Size(plrv_bo);
+ slen = strlen(plrv_sc);
+
+ Py_XDECREF(plrv_bo);
+
+ if (slen < plen)
+ ereport(ERROR,
+ (errcode(ERRCODE_DATATYPE_MISMATCH),
+ errmsg("could not convert Python object into cstring: Python string representation appears to contain null bytes")));
+ else if (slen > plen)
+ elog(ERROR, "could not convert Python object into cstring: Python string longer than reported length");
+ pg_verifymbstr(plrv_sc, slen, false);
+
+ return plrv_sc;
+}
+
+
+/*
+ * Generic output conversion function: convert PyObject to cstring and
+ * cstring into PostgreSQL type.
+ */
+static Datum
+PLyObject_ToScalar(PLyObToDatum *arg, PyObject *plrv,
+ bool *isnull, bool inarray)
+{
+ char *str;
+
+ if (plrv == Py_None)
+ {
+ *isnull = true;
+ return (Datum) 0;
+ }
+ *isnull = false;
+
+ str = PLyObject_AsString(plrv);
+
+ return InputFunctionCall(&arg->u.scalar.typfunc,
+ str,
+ arg->u.scalar.typioparam,
+ arg->typmod);
+}
+
+
+/*
+ * Convert to a domain type.
+ */
+static Datum
+PLyObject_ToDomain(PLyObToDatum *arg, PyObject *plrv,
+ bool *isnull, bool inarray)
+{
+ Datum result;
+ PLyObToDatum *base = arg->u.domain.base;
+
+ result = base->func(base, plrv, isnull, inarray);
+ domain_check(result, *isnull, arg->typoid,
+ &arg->u.domain.domain_info, arg->mcxt);
+ return result;
+}
+
+
+/*
+ * Convert using a to-SQL transform function.
+ */
+static Datum
+PLyObject_ToTransform(PLyObToDatum *arg, PyObject *plrv,
+ bool *isnull, bool inarray)
+{
+ if (plrv == Py_None)
+ {
+ *isnull = true;
+ return (Datum) 0;
+ }
+ *isnull = false;
+ return FunctionCall1(&arg->u.transform.typtransform, PointerGetDatum(plrv));
+}
+
+
+/*
+ * Convert Python sequence (or list of lists) to SQL array.
+ */
+static Datum
+PLySequence_ToArray(PLyObToDatum *arg, PyObject *plrv,
+ bool *isnull, bool inarray)
+{
+ ArrayBuildState *astate = NULL;
+ int ndims = 1;
+ int dims[MAXDIM];
+ int lbs[MAXDIM];
+
+ if (plrv == Py_None)
+ {
+ *isnull = true;
+ return (Datum) 0;
+ }
+ *isnull = false;
+
+ /*
+ * For historical reasons, we allow any sequence (not only a list) at the
+ * top level when converting a Python object to a SQL array. However, a
+ * multi-dimensional array is recognized only when the object contains
+ * true lists.
+ */
+ if (!PySequence_Check(plrv))
+ ereport(ERROR,
+ (errcode(ERRCODE_DATATYPE_MISMATCH),
+ errmsg("return value of function with array return type is not a Python sequence")));
+
+ /* Initialize dimensionality info with first-level dimension */
+ memset(dims, 0, sizeof(dims));
+ dims[0] = PySequence_Length(plrv);
+
+ /*
+ * Traverse the Python lists, in depth-first order, and collect all the
+ * elements at the bottom level into an ArrayBuildState.
+ */
+ PLySequence_ToArray_recurse(plrv, &astate,
+ &ndims, dims, 1,
+ arg->u.array.elm,
+ arg->u.array.elmbasetype);
+
+ /* ensure we get zero-D array for no inputs, as per PG convention */
+ if (astate == NULL)
+ return PointerGetDatum(construct_empty_array(arg->u.array.elmbasetype));
+
+ for (int i = 0; i < ndims; i++)
+ lbs[i] = 1;
+
+ return makeMdArrayResult(astate, ndims, dims, lbs,
+ CurrentMemoryContext, true);
+}
+
+/*
+ * Helper function for PLySequence_ToArray. Traverse a Python list of lists in
+ * depth-first order, storing the elements in *astatep.
+ *
+ * The ArrayBuildState is created only when we first find a scalar element;
+ * if we didn't do it like that, we'd need some other convention for knowing
+ * whether we'd already found any scalars (and thus the number of dimensions
+ * is frozen).
+ */
+static void
+PLySequence_ToArray_recurse(PyObject *obj, ArrayBuildState **astatep,
+ int *ndims, int *dims, int cur_depth,
+ PLyObToDatum *elm, Oid elmbasetype)
+{
+ int i;
+ int len = PySequence_Length(obj);
+
+ /* We should not get here with a non-sequence object */
+ if (len < 0)
+ PLy_elog(ERROR, "could not determine sequence length for function return value");
+
+ for (i = 0; i < len; i++)
+ {
+ /* fetch the array element */
+ PyObject *subobj = PySequence_GetItem(obj, i);
+
+ /* need PG_TRY to ensure we release the subobj's refcount */
+ PG_TRY();
+ {
+ /* multi-dimensional array? */
+ if (PyList_Check(subobj))
+ {
+ /* set size when at first element in this level, else compare */
+ if (i == 0 && *ndims == cur_depth)
+ {
+ /* array after some scalars at same level? */
+ if (*astatep != NULL)
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+ errmsg("multidimensional arrays must have array expressions with matching dimensions")));
+ /* too many dimensions? */
+ if (cur_depth >= MAXDIM)
+ ereport(ERROR,
+ (errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED),
+ errmsg("number of array dimensions exceeds the maximum allowed (%d)",
+ MAXDIM)));
+ /* OK, add a dimension */
+ dims[*ndims] = PySequence_Length(subobj);
+ (*ndims)++;
+ }
+ else if (cur_depth >= *ndims ||
+ PySequence_Length(subobj) != dims[cur_depth])
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+ errmsg("multidimensional arrays must have array expressions with matching dimensions")));
+
+ /* recurse to fetch elements of this sub-array */
+ PLySequence_ToArray_recurse(subobj, astatep,
+ ndims, dims, cur_depth + 1,
+ elm, elmbasetype);
+ }
+ else
+ {
+ Datum dat;
+ bool isnull;
+
+ /* scalar after some sub-arrays at same level? */
+ if (*ndims != cur_depth)
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+ errmsg("multidimensional arrays must have array expressions with matching dimensions")));
+
+ /* convert non-list object to Datum */
+ dat = elm->func(elm, subobj, &isnull, true);
+
+ /* create ArrayBuildState if we didn't already */
+ if (*astatep == NULL)
+ *astatep = initArrayResult(elmbasetype,
+ CurrentMemoryContext, true);
+
+ /* ... and save the element value in it */
+ (void) accumArrayResult(*astatep, dat, isnull,
+ elmbasetype, CurrentMemoryContext);
+ }
+ }
+ PG_FINALLY();
+ {
+ Py_XDECREF(subobj);
+ }
+ PG_END_TRY();
+ }
+}
+
+
+/*
+ * Convert a Python string to composite, using record_in.
+ */
+static Datum
+PLyUnicode_ToComposite(PLyObToDatum *arg, PyObject *string, bool inarray)
+{
+ char *str;
+
+ /*
+ * Set up call data for record_in, if we didn't already. (We can't just
+ * use DirectFunctionCall, because record_in needs a fn_extra field.)
+ */
+ if (!OidIsValid(arg->u.tuple.recinfunc.fn_oid))
+ fmgr_info_cxt(F_RECORD_IN, &arg->u.tuple.recinfunc, arg->mcxt);
+
+ str = PLyObject_AsString(string);
+
+ /*
+ * If we are parsing a composite type within an array, and the string
+ * isn't a valid record literal, there's a high chance that the function
+ * did something like:
+ *
+ * CREATE FUNCTION .. RETURNS comptype[] AS $$ return [['foo', 'bar']] $$
+ * LANGUAGE plpython;
+ *
+ * Before PostgreSQL 10, that was interpreted as a single-dimensional
+ * array, containing record ('foo', 'bar'). PostgreSQL 10 added support
+ * for multi-dimensional arrays, and it is now interpreted as a
+ * two-dimensional array, containing two records, 'foo', and 'bar'.
+ * record_in() will throw an error, because "foo" is not a valid record
+ * literal.
+ *
+ * To make that less confusing to users who are upgrading from older
+ * versions, try to give a hint in the typical instances of that. If we
+ * are parsing an array of composite types, and we see a string literal
+ * that is not a valid record literal, give a hint. We only want to give
+ * the hint in the narrow case of a malformed string literal, not any
+ * error from record_in(), so check for that case here specifically.
+ *
+ * This check better match the one in record_in(), so that we don't forbid
+ * literals that are actually valid!
+ */
+ if (inarray)
+ {
+ char *ptr = str;
+
+ /* Allow leading whitespace */
+ while (*ptr && isspace((unsigned char) *ptr))
+ ptr++;
+ if (*ptr++ != '(')
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+ errmsg("malformed record literal: \"%s\"", str),
+ errdetail("Missing left parenthesis."),
+ errhint("To return a composite type in an array, return the composite type as a Python tuple, e.g., \"[('foo',)]\".")));
+ }
+
+ return InputFunctionCall(&arg->u.tuple.recinfunc,
+ str,
+ arg->typoid,
+ arg->typmod);
+}
+
+
+static Datum
+PLyMapping_ToComposite(PLyObToDatum *arg, TupleDesc desc, PyObject *mapping)
+{
+ Datum result;
+ HeapTuple tuple;
+ Datum *values;
+ bool *nulls;
+ volatile int i;
+
+ Assert(PyMapping_Check(mapping));
+
+ /* Build tuple */
+ values = palloc(sizeof(Datum) * desc->natts);
+ nulls = palloc(sizeof(bool) * desc->natts);
+ for (i = 0; i < desc->natts; ++i)
+ {
+ char *key;
+ PyObject *volatile value;
+ PLyObToDatum *att;
+ Form_pg_attribute attr = TupleDescAttr(desc, i);
+
+ if (attr->attisdropped)
+ {
+ values[i] = (Datum) 0;
+ nulls[i] = true;
+ continue;
+ }
+
+ key = NameStr(attr->attname);
+ value = NULL;
+ att = &arg->u.tuple.atts[i];
+ PG_TRY();
+ {
+ value = PyMapping_GetItemString(mapping, key);
+ if (!value)
+ ereport(ERROR,
+ (errcode(ERRCODE_UNDEFINED_COLUMN),
+ errmsg("key \"%s\" not found in mapping", key),
+ errhint("To return null in a column, "
+ "add the value None to the mapping with the key named after the column.")));
+
+ values[i] = att->func(att, value, &nulls[i], false);
+
+ Py_XDECREF(value);
+ value = NULL;
+ }
+ PG_CATCH();
+ {
+ Py_XDECREF(value);
+ PG_RE_THROW();
+ }
+ PG_END_TRY();
+ }
+
+ tuple = heap_form_tuple(desc, values, nulls);
+ result = heap_copy_tuple_as_datum(tuple, desc);
+ heap_freetuple(tuple);
+
+ pfree(values);
+ pfree(nulls);
+
+ return result;
+}
+
+
+static Datum
+PLySequence_ToComposite(PLyObToDatum *arg, TupleDesc desc, PyObject *sequence)
+{
+ Datum result;
+ HeapTuple tuple;
+ Datum *values;
+ bool *nulls;
+ volatile int idx;
+ volatile int i;
+
+ Assert(PySequence_Check(sequence));
+
+ /*
+ * Check that sequence length is exactly same as PG tuple's. We actually
+ * can ignore exceeding items or assume missing ones as null but to avoid
+ * plpython developer's errors we are strict here
+ */
+ idx = 0;
+ for (i = 0; i < desc->natts; i++)
+ {
+ if (!TupleDescAttr(desc, i)->attisdropped)
+ idx++;
+ }
+ if (PySequence_Length(sequence) != idx)
+ ereport(ERROR,
+ (errcode(ERRCODE_DATATYPE_MISMATCH),
+ errmsg("length of returned sequence did not match number of columns in row")));
+
+ /* Build tuple */
+ values = palloc(sizeof(Datum) * desc->natts);
+ nulls = palloc(sizeof(bool) * desc->natts);
+ idx = 0;
+ for (i = 0; i < desc->natts; ++i)
+ {
+ PyObject *volatile value;
+ PLyObToDatum *att;
+
+ if (TupleDescAttr(desc, i)->attisdropped)
+ {
+ values[i] = (Datum) 0;
+ nulls[i] = true;
+ continue;
+ }
+
+ value = NULL;
+ att = &arg->u.tuple.atts[i];
+ PG_TRY();
+ {
+ value = PySequence_GetItem(sequence, idx);
+ Assert(value);
+
+ values[i] = att->func(att, value, &nulls[i], false);
+
+ Py_XDECREF(value);
+ value = NULL;
+ }
+ PG_CATCH();
+ {
+ Py_XDECREF(value);
+ PG_RE_THROW();
+ }
+ PG_END_TRY();
+
+ idx++;
+ }
+
+ tuple = heap_form_tuple(desc, values, nulls);
+ result = heap_copy_tuple_as_datum(tuple, desc);
+ heap_freetuple(tuple);
+
+ pfree(values);
+ pfree(nulls);
+
+ return result;
+}
+
+
+static Datum
+PLyGenericObject_ToComposite(PLyObToDatum *arg, TupleDesc desc, PyObject *object, bool inarray)
+{
+ Datum result;
+ HeapTuple tuple;
+ Datum *values;
+ bool *nulls;
+ volatile int i;
+
+ /* Build tuple */
+ values = palloc(sizeof(Datum) * desc->natts);
+ nulls = palloc(sizeof(bool) * desc->natts);
+ for (i = 0; i < desc->natts; ++i)
+ {
+ char *key;
+ PyObject *volatile value;
+ PLyObToDatum *att;
+ Form_pg_attribute attr = TupleDescAttr(desc, i);
+
+ if (attr->attisdropped)
+ {
+ values[i] = (Datum) 0;
+ nulls[i] = true;
+ continue;
+ }
+
+ key = NameStr(attr->attname);
+ value = NULL;
+ att = &arg->u.tuple.atts[i];
+ PG_TRY();
+ {
+ value = PyObject_GetAttrString(object, key);
+ if (!value)
+ {
+ /*
+ * No attribute for this column in the object.
+ *
+ * If we are parsing a composite type in an array, a likely
+ * cause is that the function contained something like "[[123,
+ * 'foo']]". Before PostgreSQL 10, that was interpreted as an
+ * array, with a composite type (123, 'foo') in it. But now
+ * it's interpreted as a two-dimensional array, and we try to
+ * interpret "123" as the composite type. See also similar
+ * heuristic in PLyObject_ToScalar().
+ */
+ ereport(ERROR,
+ (errcode(ERRCODE_UNDEFINED_COLUMN),
+ errmsg("attribute \"%s\" does not exist in Python object", key),
+ inarray ?
+ errhint("To return a composite type in an array, return the composite type as a Python tuple, e.g., \"[('foo',)]\".") :
+ errhint("To return null in a column, let the returned object have an attribute named after column with value None.")));
+ }
+
+ values[i] = att->func(att, value, &nulls[i], false);
+
+ Py_XDECREF(value);
+ value = NULL;
+ }
+ PG_CATCH();
+ {
+ Py_XDECREF(value);
+ PG_RE_THROW();
+ }
+ PG_END_TRY();
+ }
+
+ tuple = heap_form_tuple(desc, values, nulls);
+ result = heap_copy_tuple_as_datum(tuple, desc);
+ heap_freetuple(tuple);
+
+ pfree(values);
+ pfree(nulls);
+
+ return result;
+}
diff --git a/src/pl/plpython/plpy_typeio.h b/src/pl/plpython/plpy_typeio.h
new file mode 100644
index 0000000..5417f09
--- /dev/null
+++ b/src/pl/plpython/plpy_typeio.h
@@ -0,0 +1,175 @@
+/*
+ * src/pl/plpython/plpy_typeio.h
+ */
+
+#ifndef PLPY_TYPEIO_H
+#define PLPY_TYPEIO_H
+
+#include "access/htup.h"
+#include "fmgr.h"
+#include "plpython.h"
+#include "utils/typcache.h"
+
+struct PLyProcedure; /* avoid requiring plpy_procedure.h here */
+
+
+/*
+ * "Input" conversion from PostgreSQL Datum to a Python object.
+ *
+ * arg is the previously-set-up conversion data, val is the value to convert.
+ * val mustn't be NULL.
+ *
+ * Note: the conversion data structs should be regarded as private to
+ * plpy_typeio.c. We declare them here only so that other modules can
+ * define structs containing them.
+ */
+typedef struct PLyDatumToOb PLyDatumToOb; /* forward reference */
+
+typedef PyObject *(*PLyDatumToObFunc) (PLyDatumToOb *arg, Datum val);
+
+typedef struct PLyScalarToOb
+{
+ FmgrInfo typfunc; /* lookup info for type's output function */
+} PLyScalarToOb;
+
+typedef struct PLyArrayToOb
+{
+ PLyDatumToOb *elm; /* conversion info for array's element type */
+} PLyArrayToOb;
+
+typedef struct PLyTupleToOb
+{
+ /* If we're dealing with a RECORD type, actual descriptor is here: */
+ TupleDesc recdesc;
+ /* If we're dealing with a named composite type, these fields are set: */
+ TypeCacheEntry *typentry; /* typcache entry for type */
+ uint64 tupdescid; /* last tupdesc identifier seen in typcache */
+ /* These fields are NULL/0 if not yet set: */
+ PLyDatumToOb *atts; /* array of per-column conversion info */
+ int natts; /* length of array */
+} PLyTupleToOb;
+
+typedef struct PLyTransformToOb
+{
+ FmgrInfo typtransform; /* lookup info for from-SQL transform func */
+} PLyTransformToOb;
+
+struct PLyDatumToOb
+{
+ PLyDatumToObFunc func; /* conversion control function */
+ Oid typoid; /* OID of the source type */
+ int32 typmod; /* typmod of the source type */
+ bool typbyval; /* its physical representation details */
+ int16 typlen;
+ char typalign;
+ MemoryContext mcxt; /* context this info is stored in */
+ union /* conversion-type-specific data */
+ {
+ PLyScalarToOb scalar;
+ PLyArrayToOb array;
+ PLyTupleToOb tuple;
+ PLyTransformToOb transform;
+ } u;
+};
+
+/*
+ * "Output" conversion from Python object to a PostgreSQL Datum.
+ *
+ * arg is the previously-set-up conversion data, val is the value to convert.
+ *
+ * *isnull is set to true if val is Py_None, false otherwise.
+ * (The conversion function *must* be called even for Py_None,
+ * so that domain constraints can be checked.)
+ *
+ * inarray is true if the converted value was in an array (Python list).
+ * It is used to give a better error message in some cases.
+ */
+typedef struct PLyObToDatum PLyObToDatum; /* forward reference */
+
+typedef Datum (*PLyObToDatumFunc) (PLyObToDatum *arg, PyObject *val,
+ bool *isnull,
+ bool inarray);
+
+typedef struct PLyObToScalar
+{
+ FmgrInfo typfunc; /* lookup info for type's input function */
+ Oid typioparam; /* argument to pass to it */
+} PLyObToScalar;
+
+typedef struct PLyObToArray
+{
+ PLyObToDatum *elm; /* conversion info for array's element type */
+ Oid elmbasetype; /* element base type */
+} PLyObToArray;
+
+typedef struct PLyObToTuple
+{
+ /* If we're dealing with a RECORD type, actual descriptor is here: */
+ TupleDesc recdesc;
+ /* If we're dealing with a named composite type, these fields are set: */
+ TypeCacheEntry *typentry; /* typcache entry for type */
+ uint64 tupdescid; /* last tupdesc identifier seen in typcache */
+ /* These fields are NULL/0 if not yet set: */
+ PLyObToDatum *atts; /* array of per-column conversion info */
+ int natts; /* length of array */
+ /* We might need to convert using record_in(); if so, cache info here */
+ FmgrInfo recinfunc; /* lookup info for record_in */
+} PLyObToTuple;
+
+typedef struct PLyObToDomain
+{
+ PLyObToDatum *base; /* conversion info for domain's base type */
+ void *domain_info; /* cache space for domain_check() */
+} PLyObToDomain;
+
+typedef struct PLyObToTransform
+{
+ FmgrInfo typtransform; /* lookup info for to-SQL transform function */
+} PLyObToTransform;
+
+struct PLyObToDatum
+{
+ PLyObToDatumFunc func; /* conversion control function */
+ Oid typoid; /* OID of the target type */
+ int32 typmod; /* typmod of the target type */
+ bool typbyval; /* its physical representation details */
+ int16 typlen;
+ char typalign;
+ MemoryContext mcxt; /* context this info is stored in */
+ union /* conversion-type-specific data */
+ {
+ PLyObToScalar scalar;
+ PLyObToArray array;
+ PLyObToTuple tuple;
+ PLyObToDomain domain;
+ PLyObToTransform transform;
+ } u;
+};
+
+
+extern PGDLLEXPORT PyObject *PLy_input_convert(PLyDatumToOb *arg, Datum val);
+extern PGDLLEXPORT Datum PLy_output_convert(PLyObToDatum *arg, PyObject *val,
+ bool *isnull);
+
+extern PGDLLEXPORT PyObject *PLy_input_from_tuple(PLyDatumToOb *arg, HeapTuple tuple,
+ TupleDesc desc, bool include_generated);
+
+extern PGDLLEXPORT void PLy_input_setup_func(PLyDatumToOb *arg, MemoryContext arg_mcxt,
+ Oid typeOid, int32 typmod,
+ struct PLyProcedure *proc);
+extern PGDLLEXPORT void PLy_output_setup_func(PLyObToDatum *arg, MemoryContext arg_mcxt,
+ Oid typeOid, int32 typmod,
+ struct PLyProcedure *proc);
+
+extern PGDLLEXPORT void PLy_input_setup_tuple(PLyDatumToOb *arg, TupleDesc desc,
+ struct PLyProcedure *proc);
+extern PGDLLEXPORT void PLy_output_setup_tuple(PLyObToDatum *arg, TupleDesc desc,
+ struct PLyProcedure *proc);
+
+extern PGDLLEXPORT void PLy_output_setup_record(PLyObToDatum *arg, TupleDesc desc,
+ struct PLyProcedure *proc);
+
+/* conversion from Python objects to C strings --- exported for transforms */
+extern PGDLLEXPORT char *PLyObject_AsString(PyObject *plrv);
+
+#endif /* PLPY_TYPEIO_H */
diff --git a/src/pl/plpython/plpy_util.c b/src/pl/plpython/plpy_util.c
new file mode 100644
index 0000000..22e2a59
--- /dev/null
+++ b/src/pl/plpython/plpy_util.c
@@ -0,0 +1,121 @@
+/*
+ * utility functions
+ *
+ * src/pl/plpython/plpy_util.c
+ */
+
+#include "postgres.h"
+
+#include "mb/pg_wchar.h"
+#include "plpy_elog.h"
+#include "plpy_util.h"
+#include "plpython.h"
+#include "utils/memutils.h"
+
+/*
+ * Convert a Python unicode object to a Python string/bytes object in
+ * PostgreSQL server encoding. Reference ownership is passed to the
+ * caller.
+ */
+PyObject *
+PLyUnicode_Bytes(PyObject *unicode)
+{
+ PyObject *bytes,
+ *rv;
+ char *utf8string,
+ *encoded;
+
+ /* First encode the Python unicode object with UTF-8. */
+ bytes = PyUnicode_AsUTF8String(unicode);
+ if (bytes == NULL)
+ PLy_elog(ERROR, "could not convert Python Unicode object to bytes");
+
+ utf8string = PyBytes_AsString(bytes);
+ if (utf8string == NULL)
+ {
+ Py_DECREF(bytes);
+ PLy_elog(ERROR, "could not extract bytes from encoded string");
+ }
+
+ /*
+ * Then convert to server encoding if necessary.
+ *
+ * PyUnicode_AsEncodedString could be used to encode the object directly
+ * in the server encoding, but Python doesn't support all the encodings
+ * that PostgreSQL does (EUC_TW and MULE_INTERNAL). UTF-8 is used as an
+ * intermediary in PLyUnicode_FromString as well.
+ */
+ if (GetDatabaseEncoding() != PG_UTF8)
+ {
+ PG_TRY();
+ {
+ encoded = pg_any_to_server(utf8string,
+ strlen(utf8string),
+ PG_UTF8);
+ }
+ PG_CATCH();
+ {
+ Py_DECREF(bytes);
+ PG_RE_THROW();
+ }
+ PG_END_TRY();
+ }
+ else
+ encoded = utf8string;
+
+ /* finally, build a bytes object in the server encoding */
+ rv = PyBytes_FromStringAndSize(encoded, strlen(encoded));
+
+ /* if pg_any_to_server allocated memory, free it now */
+ if (utf8string != encoded)
+ pfree(encoded);
+
+ Py_DECREF(bytes);
+ return rv;
+}
+
+/*
+ * Convert a Python unicode object to a C string in PostgreSQL server
+ * encoding. No Python object reference is passed out of this
+ * function. The result is palloc'ed.
+ */
+char *
+PLyUnicode_AsString(PyObject *unicode)
+{
+ PyObject *o = PLyUnicode_Bytes(unicode);
+ char *rv = pstrdup(PyBytes_AsString(o));
+
+ Py_XDECREF(o);
+ return rv;
+}
+
+/*
+ * Convert a C string in the PostgreSQL server encoding to a Python
+ * unicode object. Reference ownership is passed to the caller.
+ */
+PyObject *
+PLyUnicode_FromStringAndSize(const char *s, Py_ssize_t size)
+{
+ char *utf8string;
+ PyObject *o;
+
+ utf8string = pg_server_to_any(s, size, PG_UTF8);
+
+ if (utf8string == s)
+ {
+ o = PyUnicode_FromStringAndSize(s, size);
+ }
+ else
+ {
+ o = PyUnicode_FromString(utf8string);
+ pfree(utf8string);
+ }
+
+ return o;
+}
+
+PyObject *
+PLyUnicode_FromString(const char *s)
+{
+ return PLyUnicode_FromStringAndSize(s, strlen(s));
+}
diff --git a/src/pl/plpython/plpy_util.h b/src/pl/plpython/plpy_util.h
new file mode 100644
index 0000000..6f491b0
--- /dev/null
+++ b/src/pl/plpython/plpy_util.h
@@ -0,0 +1,17 @@
+/*--------------------------
+ * common utility functions
+ *--------------------------
+ */
+
+#ifndef PLPY_UTIL_H
+#define PLPY_UTIL_H
+
+#include "plpython.h"
+
+extern PGDLLEXPORT PyObject *PLyUnicode_Bytes(PyObject *unicode);
+extern PGDLLEXPORT char *PLyUnicode_AsString(PyObject *unicode);
+
+extern PGDLLEXPORT PyObject *PLyUnicode_FromString(const char *s);
+extern PGDLLEXPORT PyObject *PLyUnicode_FromStringAndSize(const char *s, Py_ssize_t size);
+
+#endif /* PLPY_UTIL_H */
diff --git a/src/pl/plpython/plpython.h b/src/pl/plpython/plpython.h
new file mode 100644
index 0000000..f33f7b5
--- /dev/null
+++ b/src/pl/plpython/plpython.h
@@ -0,0 +1,37 @@
+/*-------------------------------------------------------------------------
+ *
+ * plpython.h - Python as a procedural language for PostgreSQL
+ *
+ * Portions Copyright (c) 1996-2023, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/pl/plpython/plpython.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef PLPYTHON_H
+#define PLPYTHON_H
+
+/* postgres.h needs to be included before Python.h, as usual */
+#if !defined(POSTGRES_H)
+#error postgres.h must be included before plpython.h
+#elif defined(Py_PYTHON_H)
+#error Python.h must be included via plpython.h
+#endif
+
+/*
+ * Pull in Python headers via a wrapper header, to control the scope of
+ * the system_header pragma therein.
+ */
+#include "plpython_system.h"
+
+/* define our text domain for translations */
+#undef TEXTDOMAIN
+#define TEXTDOMAIN PG_TEXTDOMAIN("plpython")
+
+/*
+ * Used throughout, so it's easier to just include it everywhere.
+ */
+#include "plpy_util.h"
+
+#endif /* PLPYTHON_H */
diff --git a/src/pl/plpython/plpython3u--1.0.sql b/src/pl/plpython/plpython3u--1.0.sql
new file mode 100644
index 0000000..ba2e6ac
--- /dev/null
+++ b/src/pl/plpython/plpython3u--1.0.sql
@@ -0,0 +1,17 @@
+/* src/pl/plpython/plpython3u--1.0.sql */
+
+CREATE FUNCTION plpython3_call_handler() RETURNS language_handler
+ LANGUAGE c AS 'MODULE_PATHNAME';
+
+CREATE FUNCTION plpython3_inline_handler(internal) RETURNS void
+ STRICT LANGUAGE c AS 'MODULE_PATHNAME';
+
+CREATE FUNCTION plpython3_validator(oid) RETURNS void
+ STRICT LANGUAGE c AS 'MODULE_PATHNAME';
+
+CREATE LANGUAGE plpython3u
+ HANDLER plpython3_call_handler
+ INLINE plpython3_inline_handler
+ VALIDATOR plpython3_validator;
+
+COMMENT ON LANGUAGE plpython3u IS 'PL/Python3U untrusted procedural language';
diff --git a/src/pl/plpython/plpython3u.control b/src/pl/plpython/plpython3u.control
new file mode 100644
index 0000000..01905ef
--- /dev/null
+++ b/src/pl/plpython/plpython3u.control
@@ -0,0 +1,7 @@
+# plpython3u extension
+comment = 'PL/Python3U untrusted procedural language'
+default_version = '1.0'
+module_pathname = '$libdir/plpython3'
+relocatable = false
+schema = pg_catalog
+superuser = true
diff --git a/src/pl/plpython/plpython_system.h b/src/pl/plpython/plpython_system.h
new file mode 100644
index 0000000..9532789
--- /dev/null
+++ b/src/pl/plpython/plpython_system.h
@@ -0,0 +1,53 @@
+/*-------------------------------------------------------------------------
+ *
+ * plpython_system.h - pull in Python's system header files
+ *
+ * We break this out as a separate header file to precisely control
+ * the scope of the "system_header" pragma. No Postgres-specific
+ * declarations should be put here. However, we do include some stuff
+ * that is meant to prevent conflicts between our code and Python.
+ *
+ * Portions Copyright (c) 1996-2023, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/pl/plpython/plpython_system.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef PLPYTHON_SYSTEM_H
+#define PLPYTHON_SYSTEM_H
+
+/*
+ * Newer versions of the Python headers trigger a lot of warnings with our
+ * preferred compiler flags (at least -Wdeclaration-after-statement is known
+ * to be problematic). The system_header pragma hides warnings from within
+ * the rest of this file, if supported.
+ */
+#ifdef HAVE_PRAGMA_GCC_SYSTEM_HEADER
+#pragma GCC system_header
+#endif
+
+/*
+ * Python versions <= 3.8 otherwise define a replacement, causing macro
+ * redefinition warnings.
+ */
+#define HAVE_SNPRINTF 1
+
+#if defined(_MSC_VER) && defined(_DEBUG)
+/* Python uses #pragma to bring in a non-default libpython on VC++ if
+ * _DEBUG is defined */
+#undef _DEBUG
+/* Also hide away errcode, since we load Python.h before postgres.h */
+#define errcode __msvc_errcode
+#include <Python.h>
+#undef errcode
+#define _DEBUG
+#elif defined (_MSC_VER)
+#define errcode __msvc_errcode
+#include <Python.h>
+#undef errcode
+#else
+#include <Python.h>
+#endif
+
+#endif /* PLPYTHON_SYSTEM_H */
diff --git a/src/pl/plpython/po/LINGUAS b/src/pl/plpython/po/LINGUAS
new file mode 100644
index 0000000..4cc8bed
--- /dev/null
+++ b/src/pl/plpython/po/LINGUAS
@@ -0,0 +1 @@
+cs de el es fr it ja ka ko pl pt_BR ru sv tr uk vi zh_CN zh_TW
diff --git a/src/pl/plpython/po/cs.po b/src/pl/plpython/po/cs.po
new file mode 100644
index 0000000..61576ac
--- /dev/null
+++ b/src/pl/plpython/po/cs.po
@@ -0,0 +1,503 @@
+# Czech message translation file for plpython
+# Copyright (C) 2012 PostgreSQL Global Development Group
+# This file is distributed under the same license as the PostgreSQL package.
+#
+# Tomáš Vondra <tv@fuzzy.cz>, 2012, 2013.
+msgid ""
+msgstr ""
+"Project-Id-Version: plpython-cs (PostgreSQL 9.3)\n"
+"Report-Msgid-Bugs-To: pgsql-bugs@lists.postgresql.org\n"
+"POT-Creation-Date: 2019-09-27 08:08+0000\n"
+"PO-Revision-Date: 2019-09-27 21:01+0200\n"
+"Last-Translator: Tomas Vondra <tv@fuzzy.cz>\n"
+"Language-Team: Czech <info@cspug.cx>\n"
+"Language: cs\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;\n"
+"X-Generator: Poedit 2.2.3\n"
+
+#: plpy_cursorobject.c:78
+#, c-format
+msgid "plpy.cursor expected a query or a plan"
+msgstr "plpy.cursor očekával dotaz nebo plán"
+
+#: plpy_cursorobject.c:161
+#, c-format
+msgid "plpy.cursor takes a sequence as its second argument"
+msgstr "plpy.cursor jako druhý argument očekává sekvenci"
+
+#: plpy_cursorobject.c:177 plpy_spi.c:211
+#, c-format
+msgid "could not execute plan"
+msgstr "nelze spustit plán"
+
+#: plpy_cursorobject.c:180 plpy_spi.c:214
+#, c-format
+msgid "Expected sequence of %d argument, got %d: %s"
+msgid_plural "Expected sequence of %d arguments, got %d: %s"
+msgstr[0] "Očekávána posloupnost %d argument, předáno %d: %s"
+msgstr[1] "Očekávána posloupnost %d argumentů, předáno %d: %s"
+msgstr[2] "Očekávána posloupnost %d argumentů, předáno %d: %s"
+
+#: plpy_cursorobject.c:329
+#, c-format
+msgid "iterating a closed cursor"
+msgstr "iterování uzavřeného kurzoru"
+
+#: plpy_cursorobject.c:337 plpy_cursorobject.c:403
+#, c-format
+msgid "iterating a cursor in an aborted subtransaction"
+msgstr "iterování kurzoru ve zrušené subtransakci"
+
+#: plpy_cursorobject.c:395
+#, c-format
+msgid "fetch from a closed cursor"
+msgstr "fetch ze zavřeného kurzoru"
+
+#: plpy_cursorobject.c:438 plpy_spi.c:409
+#, c-format
+msgid "query result has too many rows to fit in a Python list"
+msgstr "výsledek dotazu má příliš mnoho řádek a nevejde se to Python seznamu"
+
+#: plpy_cursorobject.c:490
+#, c-format
+msgid "closing a cursor in an aborted subtransaction"
+msgstr "uzavření kurzoru ve zrušené subtransakci"
+
+#: plpy_elog.c:129 plpy_elog.c:130 plpy_plpymodule.c:553
+#, c-format
+msgid "%s"
+msgstr "%s"
+
+#: plpy_exec.c:143
+#, c-format
+msgid "unsupported set function return mode"
+msgstr "nepodporovaný návratový mód funkce vracející tabulku"
+
+#: plpy_exec.c:144
+#, c-format
+msgid "PL/Python set-returning functions only support returning one value per call."
+msgstr "PL/Python funkce vracející tabulku podporují pouze vracení jedné hodnoty pro každé volání."
+
+#: plpy_exec.c:157
+#, c-format
+msgid "returned object cannot be iterated"
+msgstr "přes vrácený objekt nelze iterovat"
+
+#: plpy_exec.c:158
+#, c-format
+msgid "PL/Python set-returning functions must return an iterable object."
+msgstr "PL/Python funkce vracející tabulku musí vracet iterovatelný objekt."
+
+#: plpy_exec.c:172
+#, c-format
+msgid "error fetching next item from iterator"
+msgstr "chyba při načítání další položky z iterátoru"
+
+#: plpy_exec.c:215
+#, c-format
+msgid "PL/Python procedure did not return None"
+msgstr "PL/Python procedura nevrátila hodnotu None"
+
+#: plpy_exec.c:219
+#, c-format
+msgid "PL/Python function with return type \"void\" did not return None"
+msgstr "PL/Python funkce s návratovým typem \"void\" nevrátila hodnotu None"
+
+#: plpy_exec.c:375 plpy_exec.c:401
+#, c-format
+msgid "unexpected return value from trigger procedure"
+msgstr "neočekávaná návratová hodnota z trigger procedury"
+
+#: plpy_exec.c:376
+#, c-format
+msgid "Expected None or a string."
+msgstr "Očekáváno None nebo řetězec."
+
+#: plpy_exec.c:391
+#, c-format
+msgid "PL/Python trigger function returned \"MODIFY\" in a DELETE trigger -- ignored"
+msgstr "PL/Python trigger funkce vrátila \"MODIFY\" v DELETE triggeru -- ignorováno"
+
+#: plpy_exec.c:402
+#, c-format
+msgid "Expected None, \"OK\", \"SKIP\", or \"MODIFY\"."
+msgstr "Očekáváno None, \"OK\", \"SKIP\", nebo \"MODIFY\"."
+
+#: plpy_exec.c:452
+#, c-format
+msgid "PyList_SetItem() failed, while setting up arguments"
+msgstr "volání PyList_SetItem() selhalo během vytváření argumentů"
+
+#: plpy_exec.c:456
+#, c-format
+msgid "PyDict_SetItemString() failed, while setting up arguments"
+msgstr "volání PyDict_SetItemString() selhalo během přípravy argumentů"
+
+#: plpy_exec.c:468
+#, c-format
+msgid "function returning record called in context that cannot accept type record"
+msgstr "funkce vracející záznam byla zavolána z kontextu, který neumožňuje přijetí záznamu"
+
+#: plpy_exec.c:685
+#, c-format
+msgid "while creating return value"
+msgstr "během vytváření návratové hodnoty"
+
+#: plpy_exec.c:919
+#, c-format
+msgid "TD[\"new\"] deleted, cannot modify row"
+msgstr "TD[\"new\"] smazáno, nelze modifikovat řádek"
+
+#: plpy_exec.c:924
+#, c-format
+msgid "TD[\"new\"] is not a dictionary"
+msgstr "TD[\"new\"] není slovník"
+
+#: plpy_exec.c:951
+#, c-format
+msgid "TD[\"new\"] dictionary key at ordinal position %d is not a string"
+msgstr "TD[\"new\"] klíč slovníku na pozici %d není řetězec"
+
+#: plpy_exec.c:958
+#, c-format
+msgid "key \"%s\" found in TD[\"new\"] does not exist as a column in the triggering row"
+msgstr "klíč \"%s\" nalezený v TD[\"new\"] neexistuje jako sloupec v triggering řádku"
+
+#: plpy_exec.c:963
+#, c-format
+msgid "cannot set system attribute \"%s\""
+msgstr "nelze nastavit systémový atribut \"%s\""
+
+#: plpy_exec.c:968
+#, c-format
+msgid "cannot set generated column \"%s\""
+msgstr "nelze přiřazovat do generovaného sloupce \"%s\""
+
+#: plpy_exec.c:1026
+#, c-format
+msgid "while modifying trigger row"
+msgstr "během modifikace řádku triggeru"
+
+#: plpy_exec.c:1087
+#, c-format
+msgid "forcibly aborting a subtransaction that has not been exited"
+msgstr "vynucené ukončení subtransakce která nebyla dokončena"
+
+#: plpy_main.c:125
+#, c-format
+msgid "multiple Python libraries are present in session"
+msgstr "v session je načteno několik Python knihoven"
+
+#: plpy_main.c:126
+#, c-format
+msgid "Only one Python major version can be used in one session."
+msgstr "Pouze jedna major verze Pythonu může být použita v jedné session."
+
+#: plpy_main.c:142
+#, c-format
+msgid "untrapped error in initialization"
+msgstr "nezachycená chyba v inicializaci"
+
+#: plpy_main.c:165
+#, c-format
+msgid "could not import \"__main__\" module"
+msgstr "nepodařilo se naimportovat \"__main__\" modul"
+
+#: plpy_main.c:174
+#, c-format
+msgid "could not initialize globals"
+msgstr "nepodařilo se inicializovat globální proměnné (globals)"
+
+#: plpy_main.c:399
+#, c-format
+msgid "PL/Python procedure \"%s\""
+msgstr "PL/Python procedura \"%s\""
+
+#: plpy_main.c:402
+#, c-format
+msgid "PL/Python function \"%s\""
+msgstr "PL/Python funkce \"%s\""
+
+#: plpy_main.c:410
+#, c-format
+msgid "PL/Python anonymous code block"
+msgstr "PL/Python anonymní blok kódu"
+
+#: plpy_plpymodule.c:186 plpy_plpymodule.c:189
+#, c-format
+msgid "could not import \"plpy\" module"
+msgstr "nepodařilo se naimportovat \"plpy\" modul"
+
+#: plpy_plpymodule.c:204
+#, c-format
+msgid "could not create the spiexceptions module"
+msgstr "nepodařilo se vytvořit spiexceptions modul"
+
+#: plpy_plpymodule.c:212
+#, c-format
+msgid "could not add the spiexceptions module"
+msgstr "nepodařilo se naimportovat \"__main__\" modul"
+
+#: plpy_plpymodule.c:280
+#, c-format
+msgid "could not generate SPI exceptions"
+msgstr "nepodařilo se vygenerovat SPI výjimky"
+
+#: plpy_plpymodule.c:448
+#, c-format
+msgid "could not unpack arguments in plpy.elog"
+msgstr "nepodařilo se rozbalit argumenty v plpy.elog"
+
+#: plpy_plpymodule.c:457
+msgid "could not parse error message in plpy.elog"
+msgstr "nepodařilo se naparsovat chybovou hlášku v plpy.elog"
+
+#: plpy_plpymodule.c:474
+#, c-format
+msgid "argument 'message' given by name and position"
+msgstr "argument 'message' zadán jménem a pozicí"
+
+#: plpy_plpymodule.c:501
+#, c-format
+msgid "'%s' is an invalid keyword argument for this function"
+msgstr "'%s' je neplatný keyword argument pro tuto funkci"
+
+#: plpy_plpymodule.c:512 plpy_plpymodule.c:518
+#, c-format
+msgid "invalid SQLSTATE code"
+msgstr "chybný SQLSTATE kód"
+
+#: plpy_procedure.c:230
+#, c-format
+msgid "trigger functions can only be called as triggers"
+msgstr "funkce pro obsluhu triggerů mohou být volané pouze prostřednictvím triggerů"
+
+#: plpy_procedure.c:234
+#, c-format
+msgid "PL/Python functions cannot return type %s"
+msgstr "PL/Python funkce nemohou vracet typ %s"
+
+#: plpy_procedure.c:312
+#, c-format
+msgid "PL/Python functions cannot accept type %s"
+msgstr "PL/Python funkce nepodporují typ %s"
+
+#: plpy_procedure.c:402
+#, c-format
+msgid "could not compile PL/Python function \"%s\""
+msgstr "nelze zkompiloval PL/Python funkci \"%s\""
+
+#: plpy_procedure.c:405
+#, c-format
+msgid "could not compile anonymous PL/Python code block"
+msgstr "nelze zkompiloval anonymní PL/Python blok"
+
+#: plpy_resultobject.c:121 plpy_resultobject.c:147 plpy_resultobject.c:173
+#, c-format
+msgid "command did not produce a result set"
+msgstr "příkaz nevrátil žádný výsledek"
+
+#: plpy_spi.c:60
+#, c-format
+msgid "second argument of plpy.prepare must be a sequence"
+msgstr "druhý argument pro plpy.prepare musí být posloupnost"
+
+#: plpy_spi.c:104
+#, c-format
+msgid "plpy.prepare: type name at ordinal position %d is not a string"
+msgstr "plpy.prepare: název typu na pozici %d není řetězec"
+
+#: plpy_spi.c:176
+#, c-format
+msgid "plpy.execute expected a query or a plan"
+msgstr "plpy.execute očekávala dotaz nebo plán"
+
+#: plpy_spi.c:195
+#, c-format
+msgid "plpy.execute takes a sequence as its second argument"
+msgstr "plpy.execute jako druhý argument očekává posloupnost"
+
+#: plpy_spi.c:305
+#, c-format
+msgid "SPI_execute_plan failed: %s"
+msgstr "volání SPI_execute_plan selhalo: %s"
+
+#: plpy_spi.c:347
+#, c-format
+msgid "SPI_execute failed: %s"
+msgstr "volání SPI_execute selhalo: %s"
+
+#: plpy_subxactobject.c:97
+#, c-format
+msgid "this subtransaction has already been entered"
+msgstr "tato subtransakce již byla zahájena"
+
+#: plpy_subxactobject.c:103 plpy_subxactobject.c:161
+#, c-format
+msgid "this subtransaction has already been exited"
+msgstr "tato subtransakce již byla dokončena"
+
+#: plpy_subxactobject.c:155
+#, c-format
+msgid "this subtransaction has not been entered"
+msgstr "tato subtransakce nebyla zahájena"
+
+#: plpy_subxactobject.c:167
+#, c-format
+msgid "there is no subtransaction to exit from"
+msgstr "nenalezena subtransakce k ukončení"
+
+#: plpy_typeio.c:591
+#, c-format
+msgid "could not import a module for Decimal constructor"
+msgstr "nepodařilo se naimportovat modul pro Decimal constructor"
+
+#: plpy_typeio.c:595
+#, c-format
+msgid "no Decimal attribute in module"
+msgstr "modul nemá žádný Decimal atribut"
+
+#: plpy_typeio.c:601
+#, c-format
+msgid "conversion from numeric to Decimal failed"
+msgstr "konverze z numeric na Decimal selhala"
+
+#: plpy_typeio.c:915
+#, c-format
+msgid "could not create bytes representation of Python object"
+msgstr "nepodařilo se vytvořit bytovou reprezentaci Python objektu"
+
+#: plpy_typeio.c:1063
+#, c-format
+msgid "could not create string representation of Python object"
+msgstr "nepodařilo se vytvořit řetězcovou reprezentaci Python objektu"
+
+#: plpy_typeio.c:1074
+#, c-format
+msgid "could not convert Python object into cstring: Python string representation appears to contain null bytes"
+msgstr "nepodařilo se převést Python objekt na cstring: zdá se že řetězcová reprezentace Python objektu obsahuje null byty"
+
+#: plpy_typeio.c:1183
+#, c-format
+msgid "number of array dimensions exceeds the maximum allowed (%d)"
+msgstr "počet dimenzí pole překračuje povolené maximum (%d)"
+
+#: plpy_typeio.c:1187
+#, c-format
+msgid "could not determine sequence length for function return value"
+msgstr "nelze určit délku posloupnosti pro návratovou hodnotu funkce"
+
+#: plpy_typeio.c:1190 plpy_typeio.c:1194
+#, c-format
+msgid "array size exceeds the maximum allowed"
+msgstr "velikost pole překračuje povolené maximum"
+
+#: plpy_typeio.c:1220
+#, c-format
+msgid "return value of function with array return type is not a Python sequence"
+msgstr "návratová hodnota funkce s návratovým typem pole není Python posloupnost (sequence)"
+
+#: plpy_typeio.c:1266
+#, c-format
+msgid "wrong length of inner sequence: has length %d, but %d was expected"
+msgstr "chybná délka vnitční sekvence: má délku %d, ale očekáváno bylo %d"
+
+#: plpy_typeio.c:1268
+#, c-format
+msgid "To construct a multidimensional array, the inner sequences must all have the same length."
+msgstr "Pro vytvoření vícerozměrného pole musí mít včechny vnitřní sekvence stejnou délku."
+
+#: plpy_typeio.c:1347
+#, c-format
+msgid "malformed record literal: \"%s\""
+msgstr "chybný literál záznamu: \"%s\""
+
+#: plpy_typeio.c:1348
+#, c-format
+msgid "Missing left parenthesis."
+msgstr "Chybějící levá závorka."
+
+#: plpy_typeio.c:1349 plpy_typeio.c:1550
+#, c-format
+msgid "To return a composite type in an array, return the composite type as a Python tuple, e.g., \"[('foo',)]\"."
+msgstr "Pro vrácení kompozitního typu v poli, vracejte kompozitní typ jako Python tuple, e.g., \"[('foo',)]\"."
+
+#: plpy_typeio.c:1396
+#, c-format
+msgid "key \"%s\" not found in mapping"
+msgstr "klíč \"%s\" nenalezen v mapování"
+
+#: plpy_typeio.c:1397
+#, c-format
+msgid "To return null in a column, add the value None to the mapping with the key named after the column."
+msgstr "Pro vrácení hodnoty null ve sloupci, přidejte do mapování hodnotu None s klíčem pojmenovaným jako sloupec."
+
+#: plpy_typeio.c:1450
+#, c-format
+msgid "length of returned sequence did not match number of columns in row"
+msgstr "délka vrácené posloupnosti neodpovídala počtu sloupců v řádku"
+
+#: plpy_typeio.c:1548
+#, c-format
+msgid "attribute \"%s\" does not exist in Python object"
+msgstr "atribut \"%s\" v Python objektu neexistuje"
+
+#: plpy_typeio.c:1551
+#, c-format
+msgid "To return null in a column, let the returned object have an attribute named after column with value None."
+msgstr "Pro vrácení null ve sloupci, nechť vracený objekt má atribut pojmenovaný po sloupcis hodnotou None."
+
+#: plpy_util.c:35
+#, c-format
+msgid "could not convert Python Unicode object to bytes"
+msgstr "nelze převést Python Unicode objekt na byty"
+
+#: plpy_util.c:41
+#, c-format
+msgid "could not extract bytes from encoded string"
+msgstr "nepodařilo se získat byty z kódovaného řetězce"
+
+#~ msgid "could not initialize plpy"
+#~ msgstr "nepodařilo se inicializovat plpy"
+
+#~ msgid "could not create new Python list"
+#~ msgstr "nelze vytvořit nový Python seznam"
+
+#~ msgid "PL/Python only supports one-dimensional arrays."
+#~ msgstr "PL/Python podporuje pouze jednorozměrná pole."
+
+#~ msgid "cannot convert multidimensional array to Python list"
+#~ msgstr "vícerozměrné pole nelze převést na Python seznam (list)"
+
+#~ msgid "PL/Python does not support conversion to arrays of row types."
+#~ msgstr "PL/Python nepodporuje konverzi na pole řádkových typů."
+
+#~ msgid "could not create new dictionary"
+#~ msgstr "nepodařilo se vytvořit nový slovník"
+
+#~ msgid "unrecognized error in PLy_spi_execute_fetch_result"
+#~ msgstr "nerozpoznaná chyba v PLy_spi_execute_fetch_result"
+
+#~ msgid "plpy.prepare does not support composite types"
+#~ msgstr "plpy.prepare nepodporuje složené typy"
+
+#~ msgid "could not create the base SPI exceptions"
+#~ msgstr "nepodařilo se vygenerovat základní SPI výjimky"
+
+#~ msgid "plan.status takes no arguments"
+#~ msgstr "plan.status nepřijímá žádné argumenty"
+
+#~ msgid "could not create globals"
+#~ msgstr "nepodařilo se vytvořit globals"
+
+#~ msgid "Start a new session to use a different Python major version."
+#~ msgstr "Spouští se nová session pro použití jiné hlavní verze Pythonu."
+
+#~ msgid "This session has previously used Python major version %d, and it is now attempting to use Python major version %d."
+#~ msgstr "Tato session již dříve používala Python s hlavní verzí %d, a nyní se pokouší použít Python s hlavní verzí %d."
+
+#~ msgid "could not create new dictionary while building trigger arguments"
+#~ msgstr "během vytváření argumentů triggeru nelze vytvářet nový slovník"
diff --git a/src/pl/plpython/po/de.po b/src/pl/plpython/po/de.po
new file mode 100644
index 0000000..29962f1
--- /dev/null
+++ b/src/pl/plpython/po/de.po
@@ -0,0 +1,451 @@
+# German message translation file for plpython
+# Copyright (C) 2009 - 2019 PostgreSQL Global Development Group
+# This file is distributed under the same license as the PostgreSQL package.
+# Peter Eisentraut <peter@eisentraut.org>, 2009 - 2019.
+#
+# Use these quotes: »%s«
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: PostgreSQL 12\n"
+"Report-Msgid-Bugs-To: pgsql-bugs@lists.postgresql.org\n"
+"POT-Creation-Date: 2023-05-04 23:38+0000\n"
+"PO-Revision-Date: 2023-05-05 07:45+0200\n"
+"Last-Translator: Peter Eisentraut <peter@eisentraut.org>\n"
+"Language-Team: German <pgsql-translators@postgresql.org>\n"
+"Language: de\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=n != 1;\n"
+
+#: plpy_cursorobject.c:72
+#, c-format
+msgid "plpy.cursor expected a query or a plan"
+msgstr "plpy.cursor hat eine Anfrage oder einen Plan erwartet"
+
+#: plpy_cursorobject.c:155
+#, c-format
+msgid "plpy.cursor takes a sequence as its second argument"
+msgstr "plpy.cursor nimmt eine Sequenz als zweites Argument"
+
+#: plpy_cursorobject.c:171 plpy_spi.c:205
+#, c-format
+msgid "could not execute plan"
+msgstr "konnte Plan nicht ausführen"
+
+#: plpy_cursorobject.c:174 plpy_spi.c:208
+#, c-format
+msgid "Expected sequence of %d argument, got %d: %s"
+msgid_plural "Expected sequence of %d arguments, got %d: %s"
+msgstr[0] "Sequenz aus %d Argument erwartet, aber %d erhalten: %s"
+msgstr[1] "Sequenz aus %d Argumenten erwartet, aber %d erhalten: %s"
+
+#: plpy_cursorobject.c:321
+#, c-format
+msgid "iterating a closed cursor"
+msgstr "Iteration mit einem geschlossenen Cursor"
+
+#: plpy_cursorobject.c:329 plpy_cursorobject.c:395
+#, c-format
+msgid "iterating a cursor in an aborted subtransaction"
+msgstr "Iteration mit einem Cursor in einer abgebrochenen Transaktionen"
+
+#: plpy_cursorobject.c:387
+#, c-format
+msgid "fetch from a closed cursor"
+msgstr "Lesen aus einem geschlossenen Cursor"
+
+#: plpy_cursorobject.c:430 plpy_spi.c:401
+#, c-format
+msgid "query result has too many rows to fit in a Python list"
+msgstr "Anfrageergebnis hat zu viele Zeilen, um in eine Python-Liste zu passen"
+
+#: plpy_cursorobject.c:482
+#, c-format
+msgid "closing a cursor in an aborted subtransaction"
+msgstr "Schließen eines Cursors in einer abgebrochenen Subtransaktion"
+
+#: plpy_elog.c:125 plpy_elog.c:126 plpy_plpymodule.c:530
+#, c-format
+msgid "%s"
+msgstr "%s"
+
+#: plpy_exec.c:139
+#, c-format
+msgid "unsupported set function return mode"
+msgstr "nicht unterstützter Rückgabemodus für Funktion mit Mengenergebnis"
+
+#: plpy_exec.c:140
+#, c-format
+msgid "PL/Python set-returning functions only support returning one value per call."
+msgstr "PL/Python unterstützt für Funktionen mit Mengenergebnis nur das Zurückgeben von einem Wert pro Aufruf."
+
+#: plpy_exec.c:153
+#, c-format
+msgid "returned object cannot be iterated"
+msgstr "zurückgegebenes Objekt kann nicht iteriert werden"
+
+#: plpy_exec.c:154
+#, c-format
+msgid "PL/Python set-returning functions must return an iterable object."
+msgstr "PL/Python-Funktionen mit Mengenergebnis müssen ein iterierbares Objekt zurückgeben."
+
+#: plpy_exec.c:168
+#, c-format
+msgid "error fetching next item from iterator"
+msgstr "Fehler beim Auslesen des nächsten Elements vom Iterator"
+
+#: plpy_exec.c:211
+#, c-format
+msgid "PL/Python procedure did not return None"
+msgstr "PL/Python-Prozedur hat nicht None zurückgegeben"
+
+#: plpy_exec.c:215
+#, c-format
+msgid "PL/Python function with return type \"void\" did not return None"
+msgstr "PL/Python-Funktion mit Rückgabetyp »void« hat nicht None zurückgegeben"
+
+#: plpy_exec.c:369 plpy_exec.c:393
+#, c-format
+msgid "unexpected return value from trigger procedure"
+msgstr "unerwarteter Rückgabewert von Triggerprozedur"
+
+#: plpy_exec.c:370
+#, c-format
+msgid "Expected None or a string."
+msgstr "Erwartete None oder eine Zeichenkette."
+
+#: plpy_exec.c:383
+#, c-format
+msgid "PL/Python trigger function returned \"MODIFY\" in a DELETE trigger -- ignored"
+msgstr "PL/Python-Funktion gab in einem DELETE-Trigger \"MODIFY\" zurück -- ignoriert"
+
+#: plpy_exec.c:394
+#, c-format
+msgid "Expected None, \"OK\", \"SKIP\", or \"MODIFY\"."
+msgstr "Erwartete None, \"OK\", \"SKIP\" oder \"MODIFY\"."
+
+#: plpy_exec.c:444
+#, c-format
+msgid "PyList_SetItem() failed, while setting up arguments"
+msgstr "PyList_SetItem() fehlgeschlagen, beim Einrichten der Argumente"
+
+#: plpy_exec.c:448
+#, c-format
+msgid "PyDict_SetItemString() failed, while setting up arguments"
+msgstr "PyDict_SetItemString() fehlgeschlagen, beim Einrichten der Argumente"
+
+#: plpy_exec.c:460
+#, c-format
+msgid "function returning record called in context that cannot accept type record"
+msgstr "Funktion, die einen Record zurückgibt, in einem Zusammenhang aufgerufen, der Typ record nicht verarbeiten kann"
+
+#: plpy_exec.c:677
+#, c-format
+msgid "while creating return value"
+msgstr "beim Erzeugen des Rückgabewerts"
+
+#: plpy_exec.c:924
+#, c-format
+msgid "TD[\"new\"] deleted, cannot modify row"
+msgstr "TD[\"new\"] wurde gelöscht, kann Zeile nicht ändern"
+
+#: plpy_exec.c:929
+#, c-format
+msgid "TD[\"new\"] is not a dictionary"
+msgstr "TD[\"new\"] ist kein Dictionary"
+
+#: plpy_exec.c:954
+#, c-format
+msgid "TD[\"new\"] dictionary key at ordinal position %d is not a string"
+msgstr "Dictionary-Schlüssel auf Position %d in TD[\"new\"] ist keine Zeichenkette"
+
+#: plpy_exec.c:961
+#, c-format
+msgid "key \"%s\" found in TD[\"new\"] does not exist as a column in the triggering row"
+msgstr "in TD[\"new\"] gefundener Schlüssel »%s« existiert nicht als Spalte in der den Trigger auslösenden Zeile"
+
+#: plpy_exec.c:966
+#, c-format
+msgid "cannot set system attribute \"%s\""
+msgstr "Systemattribut »%s« kann nicht gesetzt werden"
+
+#: plpy_exec.c:971
+#, c-format
+msgid "cannot set generated column \"%s\""
+msgstr "kann generierte Spalte »%s« nicht setzen"
+
+#: plpy_exec.c:1029
+#, c-format
+msgid "while modifying trigger row"
+msgstr "beim Ändern der Triggerzeile"
+
+#: plpy_exec.c:1087
+#, c-format
+msgid "forcibly aborting a subtransaction that has not been exited"
+msgstr "Abbruch einer Subtransaktion, die nicht beendet wurde, wird erzwungen"
+
+#: plpy_main.c:109
+#, c-format
+msgid "multiple Python libraries are present in session"
+msgstr "in dieser Sitzung sind mehrere Python-Bibliotheken präsent"
+
+#: plpy_main.c:110
+#, c-format
+msgid "Only one Python major version can be used in one session."
+msgstr "Nur eine Python-Hauptversion kann in einer Sitzung verwendet werden."
+
+#: plpy_main.c:122
+#, c-format
+msgid "untrapped error in initialization"
+msgstr "nicht abgefangener Fehler bei der Initialisierung"
+
+#: plpy_main.c:145
+#, c-format
+msgid "could not import \"__main__\" module"
+msgstr "konnte Modul »__main__« nicht importieren"
+
+#: plpy_main.c:154
+#, c-format
+msgid "could not initialize globals"
+msgstr "konnte globale Objekte nicht initialisieren"
+
+#: plpy_main.c:352
+#, c-format
+msgid "PL/Python procedure \"%s\""
+msgstr "PL/Python-Prozedur »%s«"
+
+#: plpy_main.c:355
+#, c-format
+msgid "PL/Python function \"%s\""
+msgstr "PL/Python-Funktion »%s«"
+
+#: plpy_main.c:363
+#, c-format
+msgid "PL/Python anonymous code block"
+msgstr "anonymer PL/Python-Codeblock"
+
+#: plpy_plpymodule.c:168 plpy_plpymodule.c:171
+#, c-format
+msgid "could not import \"plpy\" module"
+msgstr "konnte Modul »plpy« nicht importieren"
+
+#: plpy_plpymodule.c:182
+#, c-format
+msgid "could not create the spiexceptions module"
+msgstr "konnte das Modul »spiexceptions« nicht erzeugen"
+
+#: plpy_plpymodule.c:190
+#, c-format
+msgid "could not add the spiexceptions module"
+msgstr "konnte das Modul »spiexceptions« nicht hinzufügen"
+
+#: plpy_plpymodule.c:257
+#, c-format
+msgid "could not generate SPI exceptions"
+msgstr "konnte SPI-Ausnahmen nicht erzeugen"
+
+#: plpy_plpymodule.c:425
+#, c-format
+msgid "could not unpack arguments in plpy.elog"
+msgstr "konnte Argumente in plpy.elog nicht entpacken"
+
+#: plpy_plpymodule.c:434
+msgid "could not parse error message in plpy.elog"
+msgstr "konnte Fehlermeldung in plpy.elog nicht parsen"
+
+#: plpy_plpymodule.c:451
+#, c-format
+msgid "argument 'message' given by name and position"
+msgstr "Argument »message« wurde durch Namen und Position angegeben"
+
+#: plpy_plpymodule.c:478
+#, c-format
+msgid "'%s' is an invalid keyword argument for this function"
+msgstr "»%s« ist ein ungültiges Schlüsselwortargument für diese Funktion"
+
+#: plpy_plpymodule.c:489 plpy_plpymodule.c:495
+#, c-format
+msgid "invalid SQLSTATE code"
+msgstr "ungültiger SQLSTATE-Code"
+
+#: plpy_procedure.c:225
+#, c-format
+msgid "trigger functions can only be called as triggers"
+msgstr "Triggerfunktionen können nur als Trigger aufgerufen werden"
+
+#: plpy_procedure.c:229
+#, c-format
+msgid "PL/Python functions cannot return type %s"
+msgstr "PL/Python-Funktionen können keinen Rückgabetyp %s haben"
+
+#: plpy_procedure.c:307
+#, c-format
+msgid "PL/Python functions cannot accept type %s"
+msgstr "PL/Python-Funktionen können Typ %s nicht annehmen"
+
+#: plpy_procedure.c:395
+#, c-format
+msgid "could not compile PL/Python function \"%s\""
+msgstr "konnte PL/Python-Funktion »%s« nicht kompilieren"
+
+#: plpy_procedure.c:398
+#, c-format
+msgid "could not compile anonymous PL/Python code block"
+msgstr "konnte anonymen PL/Python-Codeblock nicht kompilieren"
+
+#: plpy_resultobject.c:117 plpy_resultobject.c:143 plpy_resultobject.c:169
+#, c-format
+msgid "command did not produce a result set"
+msgstr "Befehl hat keine Ergebnismenge erzeugt"
+
+#: plpy_spi.c:56
+#, c-format
+msgid "second argument of plpy.prepare must be a sequence"
+msgstr "zweites Argument von plpy.prepare muss eine Sequenz sein"
+
+#: plpy_spi.c:98
+#, c-format
+msgid "plpy.prepare: type name at ordinal position %d is not a string"
+msgstr "plpy.prepare: Typname auf Position %d ist keine Zeichenkette"
+
+#: plpy_spi.c:170
+#, c-format
+msgid "plpy.execute expected a query or a plan"
+msgstr "plpy.execute hat eine Anfrage oder einen Plan erwartet"
+
+#: plpy_spi.c:189
+#, c-format
+msgid "plpy.execute takes a sequence as its second argument"
+msgstr "plpy.execute nimmt eine Sequenz als zweites Argument"
+
+#: plpy_spi.c:297
+#, c-format
+msgid "SPI_execute_plan failed: %s"
+msgstr "SPI_execute_plan fehlgeschlagen: %s"
+
+#: plpy_spi.c:339
+#, c-format
+msgid "SPI_execute failed: %s"
+msgstr "SPI_execute fehlgeschlagen: %s"
+
+#: plpy_subxactobject.c:92
+#, c-format
+msgid "this subtransaction has already been entered"
+msgstr "diese Subtransaktion wurde bereits begonnen"
+
+#: plpy_subxactobject.c:98 plpy_subxactobject.c:156
+#, c-format
+msgid "this subtransaction has already been exited"
+msgstr "diese Subtransaktion wurde bereits beendet"
+
+#: plpy_subxactobject.c:150
+#, c-format
+msgid "this subtransaction has not been entered"
+msgstr "diese Subtransaktion wurde nicht begonnen"
+
+#: plpy_subxactobject.c:162
+#, c-format
+msgid "there is no subtransaction to exit from"
+msgstr "es gibt keine Subtransaktion zu beenden"
+
+#: plpy_typeio.c:588
+#, c-format
+msgid "could not import a module for Decimal constructor"
+msgstr "konnte kein Modul für den »Decimal«-Konstruktor importieren"
+
+#: plpy_typeio.c:592
+#, c-format
+msgid "no Decimal attribute in module"
+msgstr "kein Attribut »Decimal« im Modul"
+
+#: plpy_typeio.c:598
+#, c-format
+msgid "conversion from numeric to Decimal failed"
+msgstr "Umwandlung von numeric in Decimal fehlgeschlagen"
+
+#: plpy_typeio.c:912
+#, c-format
+msgid "could not create bytes representation of Python object"
+msgstr "konnte Bytes-Darstellung eines Python-Objektes nicht erzeugen"
+
+#: plpy_typeio.c:1049
+#, c-format
+msgid "could not create string representation of Python object"
+msgstr "konnte Zeichenkettendarstellung eines Python-Objektes nicht erzeugen"
+
+#: plpy_typeio.c:1060
+#, c-format
+msgid "could not convert Python object into cstring: Python string representation appears to contain null bytes"
+msgstr "konnte Python-Objekt nicht in cstring umwandeln: Python-Zeichenkettendarstellung enthält anscheinend Null-Bytes"
+
+#: plpy_typeio.c:1157
+#, c-format
+msgid "return value of function with array return type is not a Python sequence"
+msgstr "Rückgabewert von Funktion mit Array-Rückgabetyp ist keine Python-Sequenz"
+
+#: plpy_typeio.c:1202
+#, c-format
+msgid "could not determine sequence length for function return value"
+msgstr "konnte Sequenzlänge für Funktionsrückgabewert nicht ermitteln"
+
+#: plpy_typeio.c:1222 plpy_typeio.c:1237 plpy_typeio.c:1253
+#, c-format
+msgid "multidimensional arrays must have array expressions with matching dimensions"
+msgstr "mehrdimensionale Arrays müssen Arraysausdrücke mit gleicher Anzahl Dimensionen haben"
+
+#: plpy_typeio.c:1227
+#, c-format
+msgid "number of array dimensions exceeds the maximum allowed (%d)"
+msgstr "Anzahl der Arraydimensionen überschreitet erlaubtes Maximum (%d)"
+
+#: plpy_typeio.c:1329
+#, c-format
+msgid "malformed record literal: \"%s\""
+msgstr "fehlerhafte Record-Konstante: »%s«"
+
+#: plpy_typeio.c:1330
+#, c-format
+msgid "Missing left parenthesis."
+msgstr "Linke Klammer fehlt."
+
+#: plpy_typeio.c:1331 plpy_typeio.c:1532
+#, c-format
+msgid "To return a composite type in an array, return the composite type as a Python tuple, e.g., \"[('foo',)]\"."
+msgstr "Um einen zusammengesetzten Typ in einem Array zurückzugeben, geben Sie den zusammengesetzten Typ als ein Python-Tupel zurück, z.B. »[('foo',)]«."
+
+#: plpy_typeio.c:1378
+#, c-format
+msgid "key \"%s\" not found in mapping"
+msgstr "Schlüssel »%s« nicht in Mapping gefunden"
+
+#: plpy_typeio.c:1379
+#, c-format
+msgid "To return null in a column, add the value None to the mapping with the key named after the column."
+msgstr "Um einen NULL-Wert in einer Spalte zurückzugeben, muss der Wert None mit einem nach der Spalte benannten Schlüssel in das Mapping eingefügt werden."
+
+#: plpy_typeio.c:1432
+#, c-format
+msgid "length of returned sequence did not match number of columns in row"
+msgstr "Länge der zurückgegebenen Sequenz hat nicht mit der Anzahl der Spalten in der Zeile übereingestimmt"
+
+#: plpy_typeio.c:1530
+#, c-format
+msgid "attribute \"%s\" does not exist in Python object"
+msgstr "Attribut »%s« existiert nicht in Python-Objekt"
+
+#: plpy_typeio.c:1533
+#, c-format
+msgid "To return null in a column, let the returned object have an attribute named after column with value None."
+msgstr "Um einen NULL-Wert in einer Spalte zurückzugeben, muss das zurückzugebende Objekt ein nach der Spalte benanntes Attribut mit dem Wert None haben."
+
+#: plpy_util.c:31
+#, c-format
+msgid "could not convert Python Unicode object to bytes"
+msgstr "konnte Python-Unicode-Objekt nicht in Bytes umwandeln"
+
+#: plpy_util.c:37
+#, c-format
+msgid "could not extract bytes from encoded string"
+msgstr "konnte kodierte Zeichenkette nicht in Bytes umwandeln"
diff --git a/src/pl/plpython/po/el.po b/src/pl/plpython/po/el.po
new file mode 100644
index 0000000..a606ad1
--- /dev/null
+++ b/src/pl/plpython/po/el.po
@@ -0,0 +1,461 @@
+# Greek message translation file for plpython
+# Copyright (C) 2021 PostgreSQL Global Development Group
+# This file is distributed under the same license as the plpython (PostgreSQL) package.
+# Georgios Kokolatos <gkokolatos@pm.me>, 2021.
+#
+#
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: plpython (PostgreSQL) 14\n"
+"Report-Msgid-Bugs-To: pgsql-bugs@lists.postgresql.org\n"
+"POT-Creation-Date: 2023-08-15 13:38+0000\n"
+"PO-Revision-Date: 2023-08-15 16:14+0200\n"
+"Last-Translator: Georgios Kokolatos <gkokolatos@pm.me>\n"
+"Language-Team: \n"
+"Language: el\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=n != 1;\n"
+"X-Generator: Poedit 3.3.2\n"
+
+#: plpy_cursorobject.c:72
+#, c-format
+msgid "plpy.cursor expected a query or a plan"
+msgstr "plpy.cursor ανέμενε ένα ερώτημα ή ένα σχέδιο"
+
+#: plpy_cursorobject.c:155
+#, c-format
+msgid "plpy.cursor takes a sequence as its second argument"
+msgstr "plpy.cursor λαμβάνει μια ακολουθία ως τη δεύτερη παράμετρο"
+
+#: plpy_cursorobject.c:171 plpy_spi.c:205
+#, c-format
+msgid "could not execute plan"
+msgstr "δεν ήταν δυνατή η εκτέλεση του σχεδίου"
+
+#: plpy_cursorobject.c:174 plpy_spi.c:208
+#, c-format
+msgid "Expected sequence of %d argument, got %d: %s"
+msgid_plural "Expected sequence of %d arguments, got %d: %s"
+msgstr[0] "Ανέμενε ακολουθία %d παραμέτρου, έλαβε %d: %s"
+msgstr[1] "Ανέμενε ακολουθία %d παραμέτρων, έλαβε %d: %s"
+
+#: plpy_cursorobject.c:321
+#, c-format
+msgid "iterating a closed cursor"
+msgstr "επαναλαμβάνει έναν κλειστό δρομέα"
+
+#: plpy_cursorobject.c:329 plpy_cursorobject.c:395
+#, c-format
+msgid "iterating a cursor in an aborted subtransaction"
+msgstr "επαναλαμβάνει ένα δρομέα σε μία ματαιωμένη υποσυναλλαγή"
+
+#: plpy_cursorobject.c:387
+#, c-format
+msgid "fetch from a closed cursor"
+msgstr "ανάκτηση από κλειστό δρομέα"
+
+#: plpy_cursorobject.c:430 plpy_spi.c:401
+#, c-format
+msgid "query result has too many rows to fit in a Python list"
+msgstr "το αποτέλεσμα του ερωτήματος έχει πάρα πολλές σειρές για να χωρέσει σε μια λίστα Python"
+
+#: plpy_cursorobject.c:482
+#, c-format
+msgid "closing a cursor in an aborted subtransaction"
+msgstr "κλείσιμο ενός δρομέα σε μία ματαιωμένη υποσυναλλαγή"
+
+#: plpy_elog.c:125 plpy_elog.c:126 plpy_plpymodule.c:530
+#, c-format
+msgid "%s"
+msgstr "%s"
+
+#: plpy_exec.c:139
+#, c-format
+msgid "unsupported set function return mode"
+msgstr "μη υποστηριζόμενο είδος επιστροφής συνάρτησης συνόλου"
+
+#: plpy_exec.c:140
+#, c-format
+msgid "PL/Python set-returning functions only support returning one value per call."
+msgstr "Οι λειτουργίες επιστροφής συνόλου PL/Python υποστηρίζουν μόνο την επιστροφή μίας τιμής ανά κλήση."
+
+#: plpy_exec.c:153
+#, c-format
+msgid "returned object cannot be iterated"
+msgstr "το επιστρεφόμενο αντικείμενο δεν μπορεί να επαναληφθεί"
+
+#: plpy_exec.c:154
+#, c-format
+msgid "PL/Python set-returning functions must return an iterable object."
+msgstr "Οι συναρτήσεις επιστροφής συνόλου PL/Python επιβάλλεται να επιστρέφουν ένα επαναλαμβανόμενο αντικείμενο."
+
+#: plpy_exec.c:168
+#, c-format
+msgid "error fetching next item from iterator"
+msgstr "σφάλμα κατά τη λήψη του επόμενου στοιχείου από το πρόγραμμα λήψης"
+
+#: plpy_exec.c:211
+#, c-format
+msgid "PL/Python procedure did not return None"
+msgstr "διεργασία PL/Python δεν επέστρεψε None"
+
+#: plpy_exec.c:215
+#, c-format
+msgid "PL/Python function with return type \"void\" did not return None"
+msgstr "συνάρτηση PL/Python με τύπο επιστροφής «void» δεν επέστρεψε None"
+
+#: plpy_exec.c:369 plpy_exec.c:393
+#, c-format
+msgid "unexpected return value from trigger procedure"
+msgstr "μη αναμενόμενη τιμή επιστροφής από διεργασία εναύσματος"
+
+#: plpy_exec.c:370
+#, c-format
+msgid "Expected None or a string."
+msgstr "Αναμενόταν None ή μία συμβολοσειρά."
+
+#: plpy_exec.c:383
+#, c-format
+msgid "PL/Python trigger function returned \"MODIFY\" in a DELETE trigger -- ignored"
+msgstr "συνάρτηση εναύσματος PL/Python επέστρεψε «MODIFY» σε ένα έναυσμα DELETE -- παραβλέφθηκε"
+
+#: plpy_exec.c:394
+#, c-format
+msgid "Expected None, \"OK\", \"SKIP\", or \"MODIFY\"."
+msgstr "Αναμενόταν None, «OK», «SKIP», ή «MODIFY»."
+
+#: plpy_exec.c:444
+#, c-format
+msgid "PyList_SetItem() failed, while setting up arguments"
+msgstr "PyList_SetItem() απέτυχε, κατά τη διάρκεια ετοιμασίας των παραμέτρων"
+
+#: plpy_exec.c:448
+#, c-format
+msgid "PyDict_SetItemString() failed, while setting up arguments"
+msgstr "PyDict_SetItemString() απέτυχε, κατά τη διάρκεια ετοιμασίας των παραμέτρων"
+
+#: plpy_exec.c:460
+#, c-format
+msgid "function returning record called in context that cannot accept type record"
+msgstr "συνάρτηση που επιστρέφει εγγραφή καλείται σε περιεχόμενο που δεν δύναται να αποδεχτεί τύπο εγγραφής"
+
+#: plpy_exec.c:677
+#, c-format
+msgid "while creating return value"
+msgstr "κατά τη δημιουργία τιμής επιστροφής"
+
+#: plpy_exec.c:924
+#, c-format
+msgid "TD[\"new\"] deleted, cannot modify row"
+msgstr "TD[«new»] διαγράφηκε, δεν είναι δυνατή η μετατροπή σειράς"
+
+#: plpy_exec.c:929
+#, c-format
+msgid "TD[\"new\"] is not a dictionary"
+msgstr "TD[«new»] δεν είναι ένα λεξικό"
+
+#: plpy_exec.c:954
+#, c-format
+msgid "TD[\"new\"] dictionary key at ordinal position %d is not a string"
+msgstr "TD[«new»] κλειδί λεξικού στη τακτή θέση %d δεν είναι μία συμβολοσειρά"
+
+#: plpy_exec.c:961
+#, c-format
+msgid "key \"%s\" found in TD[\"new\"] does not exist as a column in the triggering row"
+msgstr "κλειδί «%s» που βρίσκεται στο TD[\"New\"] δεν υπάρχει ως στήλη στη γραμμή εναύσματος"
+
+#: plpy_exec.c:966
+#, c-format
+msgid "cannot set system attribute \"%s\""
+msgstr "δεν είναι δυνατός ο ορισμός του χαρακτηριστικού συστήματος «%s»"
+
+#: plpy_exec.c:971
+#, c-format
+msgid "cannot set generated column \"%s\""
+msgstr "δεν είναι δυνατός ο ορισμός δημιουργημένης στήλης «%s»"
+
+#: plpy_exec.c:1029
+#, c-format
+msgid "while modifying trigger row"
+msgstr "κατά την τροποποίηση εναύσματος σειράς"
+
+#: plpy_exec.c:1087
+#, c-format
+msgid "forcibly aborting a subtransaction that has not been exited"
+msgstr "βίαιη ματαίωση μιας υποσυναλλαγής που δεν έχει εξέλθει"
+
+#: plpy_main.c:109
+#, c-format
+msgid "multiple Python libraries are present in session"
+msgstr "υπάρχουν πολλαπλές βιβλιοθήκες Python στη συνεδρία"
+
+#: plpy_main.c:110
+#, c-format
+msgid "Only one Python major version can be used in one session."
+msgstr "Μόνο μία κύρια έκδοση Python μπορεί να χρησιμοποιηθεί σε μία συνεδρία."
+
+#: plpy_main.c:122
+#, c-format
+msgid "untrapped error in initialization"
+msgstr "μη παγιδευμένο σφάλμα κατά την προετοιμασία"
+
+#: plpy_main.c:145
+#, c-format
+msgid "could not import \"__main__\" module"
+msgstr "δεν ήταν δυνατή η εισαγωγή του αρθρώματος «__main__»"
+
+#: plpy_main.c:154
+#, c-format
+msgid "could not initialize globals"
+msgstr "δεν ήταν δυνατή η αρχικοποίηση καθολικών μεταβλητών"
+
+#: plpy_main.c:352
+#, c-format
+msgid "PL/Python procedure \"%s\""
+msgstr "διεργασία PL/Python «%s»"
+
+#: plpy_main.c:355
+#, c-format
+msgid "PL/Python function \"%s\""
+msgstr "συνάρτηση PL/Python «%s»"
+
+#: plpy_main.c:363
+#, c-format
+msgid "PL/Python anonymous code block"
+msgstr "ονώνυμο μπλοκ κώδικα PL/Python"
+
+#: plpy_plpymodule.c:168 plpy_plpymodule.c:171
+#, c-format
+msgid "could not import \"plpy\" module"
+msgstr "δεν ήταν δυνατή η εισαγωγή του αρθρώματος «plpy»"
+
+#: plpy_plpymodule.c:182
+#, c-format
+msgid "could not create the spiexceptions module"
+msgstr "δεν ήταν δυνατή η δημιουργία του αρθρώματος spiexceptions"
+
+#: plpy_plpymodule.c:190
+#, c-format
+msgid "could not add the spiexceptions module"
+msgstr "δεν ήταν δυνατή η πρόσθεση του αρθρώματος spiexceptions"
+
+#: plpy_plpymodule.c:257
+#, c-format
+msgid "could not generate SPI exceptions"
+msgstr "δεν ήταν δυνατή η του αρθρώματος spiexceptions"
+
+#: plpy_plpymodule.c:425
+#, c-format
+msgid "could not unpack arguments in plpy.elog"
+msgstr "δεν ήταν δυνατή η αποσυσκευασία παραμέτρων στο plpy.elog"
+
+#: plpy_plpymodule.c:434
+msgid "could not parse error message in plpy.elog"
+msgstr "δεν ήταν δυνατή η ανάλυση μηνυμάτος σφάλματος στο plpy.elog"
+
+#: plpy_plpymodule.c:451
+#, c-format
+msgid "argument 'message' given by name and position"
+msgstr "παράμετρος «μήνυμα» που δόθηκε με όνομα και θέση"
+
+#: plpy_plpymodule.c:478
+#, c-format
+msgid "'%s' is an invalid keyword argument for this function"
+msgstr "‘%s’ είναι μία άκυρη παράμετρος με λέξη-κλειδί για αυτή τη συνάρτηση"
+
+#: plpy_plpymodule.c:489 plpy_plpymodule.c:495
+#, c-format
+msgid "invalid SQLSTATE code"
+msgstr "μη έγκυρος κωδικός SQLSTATE"
+
+#: plpy_procedure.c:225
+#, c-format
+msgid "trigger functions can only be called as triggers"
+msgstr "συναρτήσεις εναυσμάτων μπορούν να κληθούν μόνο ως εναύσματα"
+
+#: plpy_procedure.c:229
+#, c-format
+msgid "PL/Python functions cannot return type %s"
+msgstr "οι συναρτήσεις PL/Python δεν μπορούν να επιστρέψουν τύπο %s"
+
+#: plpy_procedure.c:307
+#, c-format
+msgid "PL/Python functions cannot accept type %s"
+msgstr "οι συναρτήσεις PL/Python δεν μπορούν να δεχθούν τύπο %s"
+
+#: plpy_procedure.c:395
+#, c-format
+msgid "could not compile PL/Python function \"%s\""
+msgstr "δεν ήταν δυνατή η μεταγλώττιση της συνάρτησης PL/Python «%s»"
+
+#: plpy_procedure.c:398
+#, c-format
+msgid "could not compile anonymous PL/Python code block"
+msgstr "δεν ήταν δυνατή η μεταγλώττιση ανώνυμου μπλοκ κώδικα PL/Python"
+
+#: plpy_resultobject.c:117 plpy_resultobject.c:143 plpy_resultobject.c:169
+#, c-format
+msgid "command did not produce a result set"
+msgstr "η εντολή δεν παρήγαγε ένα σύνολο αποτελάσματων"
+
+#: plpy_spi.c:56
+#, c-format
+msgid "second argument of plpy.prepare must be a sequence"
+msgstr "η δεύτερη παράμετρος του plpy.prepare επιβάλλεται να είναι μία ακολουθία"
+
+#: plpy_spi.c:98
+#, c-format
+msgid "plpy.prepare: type name at ordinal position %d is not a string"
+msgstr "plpy.prepare: το όνομα τύπου στη τακτή θέση %d δεν είναι συμβολοσειρά"
+
+#: plpy_spi.c:170
+#, c-format
+msgid "plpy.execute expected a query or a plan"
+msgstr "plpy.execute ανέμενε ένα ερώτημα ή ένα σχέδιο"
+
+#: plpy_spi.c:189
+#, c-format
+msgid "plpy.execute takes a sequence as its second argument"
+msgstr "plpy.execute λαμβάνει μια ακολουθία ως δεύτερη παράμετρό του"
+
+#: plpy_spi.c:297
+#, c-format
+msgid "SPI_execute_plan failed: %s"
+msgstr "SPI_execute_plan απέτυχε: %s"
+
+#: plpy_spi.c:339
+#, c-format
+msgid "SPI_execute failed: %s"
+msgstr "SPI_execute απέτυχε: %s"
+
+#: plpy_subxactobject.c:92
+#, c-format
+msgid "this subtransaction has already been entered"
+msgstr "έχει εισέλθει ήδη σε αυτή τη υποσυναλλάγή"
+
+#: plpy_subxactobject.c:98 plpy_subxactobject.c:156
+#, c-format
+msgid "this subtransaction has already been exited"
+msgstr "έχει εξέλθει ήδη από αυτή τη υποσυναλλάγή"
+
+#: plpy_subxactobject.c:150
+#, c-format
+msgid "this subtransaction has not been entered"
+msgstr "δεν έχει εισέλθει σε αυτή τη υποσυναλλάγή"
+
+#: plpy_subxactobject.c:162
+#, c-format
+msgid "there is no subtransaction to exit from"
+msgstr "δεν υπάρχει υποσυναλλαγή από την οποία να εξέλθει"
+
+#: plpy_typeio.c:588
+#, c-format
+msgid "could not import a module for Decimal constructor"
+msgstr "δεν ήταν δυνατή η εισαγωγή αρθρώματος για τον κατασκευαστή Decimal"
+
+#: plpy_typeio.c:592
+#, c-format
+msgid "no Decimal attribute in module"
+msgstr "καμία ιδιότητα Decimal στο άρθρωμα"
+
+#: plpy_typeio.c:598
+#, c-format
+msgid "conversion from numeric to Decimal failed"
+msgstr "μετατροπή από αριθμητικό σε Decimal απέτυχε"
+
+#: plpy_typeio.c:912
+#, c-format
+msgid "could not create bytes representation of Python object"
+msgstr "δεν ήταν δυνατή η δημιουργία bytes αναπαράστασης του αντικειμένου Python"
+
+#: plpy_typeio.c:1049
+#, c-format
+msgid "could not create string representation of Python object"
+msgstr "δεν ήταν δυνατή η δημιουργία string αναπαράστασης του αντικειμένου Python"
+
+#: plpy_typeio.c:1060
+#, c-format
+msgid "could not convert Python object into cstring: Python string representation appears to contain null bytes"
+msgstr "δεν ήταν δυνατή η μετατροπή του αντικειμένου Python σε cstring: Η αναπαράσταση συμβολοσειράς Python φαίνεται να περιέχει null bytes"
+
+#: plpy_typeio.c:1157
+#, c-format
+msgid "return value of function with array return type is not a Python sequence"
+msgstr "η τιμή επιστροφής της συνάρτησης με τύπο επιστροφής συστυχίας δεν είναι μία ακολουθία Python"
+
+#: plpy_typeio.c:1202
+#, c-format
+msgid "could not determine sequence length for function return value"
+msgstr "δεν ήταν δυνατός ο προσδιορισμός του μήκους της ακολουθίας για την τιμή επιστροφής της συνάρτησης"
+
+#: plpy_typeio.c:1222 plpy_typeio.c:1237 plpy_typeio.c:1253
+#, c-format
+msgid "multidimensional arrays must have array expressions with matching dimensions"
+msgstr "πολυδιάστατες συστυχίες πρέπει να περιέχουν εκφράσεις συστυχίας με αντίστοιχο αριθμό διαστάσεων"
+
+#: plpy_typeio.c:1227
+#, c-format
+msgid "number of array dimensions exceeds the maximum allowed (%d)"
+msgstr "o αριθμός των διαστάσεων συστοιχίας υπερβαίνει το μέγιστο επιτρεπόμενο (%d)"
+
+#: plpy_typeio.c:1329
+#, c-format
+msgid "malformed record literal: \"%s\""
+msgstr "κακοσχηματισμένο εγγραφή: «%s»"
+
+#: plpy_typeio.c:1330
+#, c-format
+msgid "Missing left parenthesis."
+msgstr "Λείπει δεξιά παρένθεση."
+
+#: plpy_typeio.c:1331 plpy_typeio.c:1532
+#, c-format
+msgid "To return a composite type in an array, return the composite type as a Python tuple, e.g., \"[('foo',)]\"."
+msgstr "Για να επιστρέψετε έναν σύνθετο τύπο σε μία συστυχία, επιστρέψτε τον σύνθετο τύπο ως πλειάδα Python, π.χ. «[(‘foo’,)»."
+
+#: plpy_typeio.c:1378
+#, c-format
+msgid "key \"%s\" not found in mapping"
+msgstr "κλειδί «%s» δεν βρέθηκε σε αντιστοίχιση"
+
+#: plpy_typeio.c:1379
+#, c-format
+msgid "To return null in a column, add the value None to the mapping with the key named after the column."
+msgstr "Για να επιστρέψετε null σε μια στήλη, προσθέστε την τιμή None στην αντιστοίχιση με το κλειδί που ονομάστηκε από τη στήλη."
+
+#: plpy_typeio.c:1432
+#, c-format
+msgid "length of returned sequence did not match number of columns in row"
+msgstr "το μήκος της ακολουθίας που επιστράφηκε δεν ταίριαζε με τον αριθμό των στηλών στη γραμμή"
+
+#: plpy_typeio.c:1530
+#, c-format
+msgid "attribute \"%s\" does not exist in Python object"
+msgstr "η ιδιότητα «%s» δεν υπάρχει στο αντικείμενο Python"
+
+#: plpy_typeio.c:1533
+#, c-format
+msgid "To return null in a column, let the returned object have an attribute named after column with value None."
+msgstr "Για να επιστρέψετε null σε μια στήλη, αφήστε το αντικείμενο που επιστράφηκε να έχει ένα χαρακτηριστικό που ονομάζεται από την στήλη με τιμή None."
+
+#: plpy_util.c:31
+#, c-format
+msgid "could not convert Python Unicode object to bytes"
+msgstr "δεν ήταν δυνατή η μετατροπή του αντικειμένου Python Unicode σε bytes"
+
+#: plpy_util.c:37
+#, c-format
+msgid "could not extract bytes from encoded string"
+msgstr "δεν ήταν δυνατή η εξόρυξη bytes από κωδικοποιημένη συμβολοσειρά"
+
+#~ msgid "To construct a multidimensional array, the inner sequences must all have the same length."
+#~ msgstr "Για να κατασκευαστεί μια πολυδιάστατη συστυχία, οι εσωτερικές ακολουθίες πρέπει να έχουν όλες το ίδιο μήκος."
+
+#~ msgid "array size exceeds the maximum allowed"
+#~ msgstr "το μέγεθος συστοιχίας υπερβαίνει το μέγιστο επιτρεπόμενο"
+
+#~ msgid "wrong length of inner sequence: has length %d, but %d was expected"
+#~ msgstr "λάθος μήκος της εσωτερικής ακολουθίας: έχει μήκος %d , αλλά αναμενόταν %d"
diff --git a/src/pl/plpython/po/es.po b/src/pl/plpython/po/es.po
new file mode 100644
index 0000000..8186ef8
--- /dev/null
+++ b/src/pl/plpython/po/es.po
@@ -0,0 +1,454 @@
+# Spanish message translation file for plpython
+#
+# Copyright (c) 2009-2021, PostgreSQL Global Development Group
+# This file is distributed under the same license as the PostgreSQL package.
+#
+# Emanuel Calvo Franco <postgres.arg@gmail.com>, 2009.
+# Alvaro Herrera <alvherre@alvh.no-ip.org>, 2009-2012
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: plpython (PostgreSQL) 16\n"
+"Report-Msgid-Bugs-To: pgsql-bugs@lists.postgresql.org\n"
+"POT-Creation-Date: 2023-05-22 07:08+0000\n"
+"PO-Revision-Date: 2023-05-22 12:06+0200\n"
+"Last-Translator: Carlos Chapi <carlos.chapi@2ndquadrant.com>\n"
+"Language-Team: PgSQL-es-Ayuda <pgsql-es-ayuda@lists.postgresql.org>\n"
+"Language: es\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=n != 1;\n"
+"X-Generator: Poedit 2.0.2\n"
+
+#: plpy_cursorobject.c:72
+#, c-format
+msgid "plpy.cursor expected a query or a plan"
+msgstr "plpy.cursor espera una consulta o un plan"
+
+#: plpy_cursorobject.c:155
+#, c-format
+msgid "plpy.cursor takes a sequence as its second argument"
+msgstr "plpy.cursor lleva una secuencia como segundo argumento"
+
+#: plpy_cursorobject.c:171 plpy_spi.c:205
+#, c-format
+msgid "could not execute plan"
+msgstr "no se pudo ejecutar el plan"
+
+#: plpy_cursorobject.c:174 plpy_spi.c:208
+#, c-format
+msgid "Expected sequence of %d argument, got %d: %s"
+msgid_plural "Expected sequence of %d arguments, got %d: %s"
+msgstr[0] "Se esperaba una secuencia de %d argumento, se obtuvo %d: %s"
+msgstr[1] "Se esperaba una secuencia de %d argumentos, se obtuvo %d: %s"
+
+#: plpy_cursorobject.c:321
+#, c-format
+msgid "iterating a closed cursor"
+msgstr "iterando un cursor cerrado"
+
+#: plpy_cursorobject.c:329 plpy_cursorobject.c:395
+#, c-format
+msgid "iterating a cursor in an aborted subtransaction"
+msgstr "iterando un cursor en una subtransacción abortada"
+
+#: plpy_cursorobject.c:387
+#, c-format
+msgid "fetch from a closed cursor"
+msgstr "haciendo «fetch» en un cursor cerrado"
+
+#: plpy_cursorobject.c:430 plpy_spi.c:401
+#, c-format
+msgid "query result has too many rows to fit in a Python list"
+msgstr "el resultado de la consulta tiene demasiados registros y no entran en una lista de Python"
+
+#: plpy_cursorobject.c:482
+#, c-format
+msgid "closing a cursor in an aborted subtransaction"
+msgstr "cerrando un cursor en una subtransacción abortada"
+
+#: plpy_elog.c:125 plpy_elog.c:126 plpy_plpymodule.c:530
+#, c-format
+msgid "%s"
+msgstr "%s"
+
+#: plpy_exec.c:139
+#, c-format
+msgid "unsupported set function return mode"
+msgstr "modo de retorno de conjunto de función no soportado"
+
+#: plpy_exec.c:140
+#, c-format
+msgid "PL/Python set-returning functions only support returning one value per call."
+msgstr "Las funciones PL/Python que retornan conjuntos sólo permiten retornar un valor por invocación."
+
+#: plpy_exec.c:153
+#, c-format
+msgid "returned object cannot be iterated"
+msgstr "objeto retornado no puede ser iterado"
+
+#: plpy_exec.c:154
+#, c-format
+msgid "PL/Python set-returning functions must return an iterable object."
+msgstr "Los funciones PL/Python que retornan conjuntos deben retornar un objeto iterable."
+
+#: plpy_exec.c:168
+#, c-format
+msgid "error fetching next item from iterator"
+msgstr "error extrayendo el próximo elemento del iterador"
+
+#: plpy_exec.c:211
+#, c-format
+msgid "PL/Python procedure did not return None"
+msgstr "procedimiento PL/Python no returnó None"
+
+#: plpy_exec.c:215
+#, c-format
+msgid "PL/Python function with return type \"void\" did not return None"
+msgstr "función PL/Python con tipo de retorno «void» no retorna None"
+
+#: plpy_exec.c:369 plpy_exec.c:393
+#, c-format
+msgid "unexpected return value from trigger procedure"
+msgstr "valor de retorno no esperado desde el procedimiento disparador"
+
+#: plpy_exec.c:370
+#, c-format
+msgid "Expected None or a string."
+msgstr "Se esperaba None o una cadena."
+
+#: plpy_exec.c:383
+#, c-format
+msgid "PL/Python trigger function returned \"MODIFY\" in a DELETE trigger -- ignored"
+msgstr "función de disparador de PL/Python retorno «MODIFY» en un disparador de tipo DELETE -- ignorado"
+
+#: plpy_exec.c:394
+#, c-format
+msgid "Expected None, \"OK\", \"SKIP\", or \"MODIFY\"."
+msgstr "Se esperaba None, «OK», «SKIP» o «MODIFY»."
+
+#: plpy_exec.c:444
+#, c-format
+msgid "PyList_SetItem() failed, while setting up arguments"
+msgstr "PyList_SetItem() falló, mientras se inicializaban los argumentos"
+
+#: plpy_exec.c:448
+#, c-format
+msgid "PyDict_SetItemString() failed, while setting up arguments"
+msgstr "PyDict_SetItemString() falló, mientras se inicializaban los argumentos"
+
+#: plpy_exec.c:460
+#, c-format
+msgid "function returning record called in context that cannot accept type record"
+msgstr "se llamó una función que retorna un registro en un contexto que no puede aceptarlo"
+
+#: plpy_exec.c:677
+#, c-format
+msgid "while creating return value"
+msgstr "mientras se creaba el valor de retorno"
+
+#: plpy_exec.c:924
+#, c-format
+msgid "TD[\"new\"] deleted, cannot modify row"
+msgstr "TD[\"new\"] borrado, no se puede modicar el registro"
+
+#: plpy_exec.c:929
+#, c-format
+msgid "TD[\"new\"] is not a dictionary"
+msgstr "TD[\"new\"] no es un diccionario"
+
+#: plpy_exec.c:954
+#, c-format
+msgid "TD[\"new\"] dictionary key at ordinal position %d is not a string"
+msgstr "el nombre del atributo de TD[\"new\"] en la posición %d no es una cadena"
+
+#: plpy_exec.c:961
+#, c-format
+msgid "key \"%s\" found in TD[\"new\"] does not exist as a column in the triggering row"
+msgstr "la llave «%s» en TD[\"new\"] no existe como columna en la fila disparadora"
+
+#: plpy_exec.c:966
+#, c-format
+msgid "cannot set system attribute \"%s\""
+msgstr "no se puede definir el atributo de sistema «%s»"
+
+#: plpy_exec.c:971
+#, c-format
+msgid "cannot set generated column \"%s\""
+msgstr "no se puede definir el atributo generado «%s»"
+
+#: plpy_exec.c:1029
+#, c-format
+msgid "while modifying trigger row"
+msgstr "mientras se modificaba la fila de disparador"
+
+# FIXME not very happy with this
+#: plpy_exec.c:1087
+#, c-format
+msgid "forcibly aborting a subtransaction that has not been exited"
+msgstr "abortando una subtransacción que no se ha cerrado"
+
+#: plpy_main.c:109
+#, c-format
+msgid "multiple Python libraries are present in session"
+msgstr "hay múltiples librerías de Python presentes en esta sesión"
+
+#: plpy_main.c:110
+#, c-format
+msgid "Only one Python major version can be used in one session."
+msgstr "Sólo se puede usar una versión mayor de Python en cada sesión."
+
+#: plpy_main.c:122
+#, c-format
+msgid "untrapped error in initialization"
+msgstr "error no capturado en la inicialización"
+
+#: plpy_main.c:145
+#, c-format
+msgid "could not import \"__main__\" module"
+msgstr "no se pudo importar el módulo «__main__»"
+
+#: plpy_main.c:154
+#, c-format
+msgid "could not initialize globals"
+msgstr "no se pudo inicializar las globales"
+
+#: plpy_main.c:352
+#, c-format
+msgid "PL/Python procedure \"%s\""
+msgstr "procedimiento PL/Python «%s»"
+
+#: plpy_main.c:355
+#, c-format
+msgid "PL/Python function \"%s\""
+msgstr "función PL/Python «%s»"
+
+#: plpy_main.c:363
+#, c-format
+msgid "PL/Python anonymous code block"
+msgstr "bloque de código anónimo de PL/Python"
+
+#: plpy_plpymodule.c:168 plpy_plpymodule.c:171
+#, c-format
+msgid "could not import \"plpy\" module"
+msgstr "no se pudo importar el módulo «plpy»"
+
+#: plpy_plpymodule.c:182
+#, c-format
+msgid "could not create the spiexceptions module"
+msgstr "no se pudo crear el módulo spiexceptions"
+
+#: plpy_plpymodule.c:190
+#, c-format
+msgid "could not add the spiexceptions module"
+msgstr "no se pudo importar el módulo spiexceptions"
+
+#: plpy_plpymodule.c:257
+#, c-format
+msgid "could not generate SPI exceptions"
+msgstr "no se pudo generar excepciones SPI"
+
+#: plpy_plpymodule.c:425
+#, c-format
+msgid "could not unpack arguments in plpy.elog"
+msgstr "no se pudo desempaquetar los argumentos de plpy.elog"
+
+#: plpy_plpymodule.c:434
+msgid "could not parse error message in plpy.elog"
+msgstr "no se pudo analizar el mensaje de error de plpy.elog"
+
+#: plpy_plpymodule.c:451
+#, c-format
+msgid "argument 'message' given by name and position"
+msgstr "el argumento 'message' fue pasado por nombre y posición"
+
+#: plpy_plpymodule.c:478
+#, c-format
+msgid "'%s' is an invalid keyword argument for this function"
+msgstr "«%s» no es un argumento válido para esta función"
+
+#: plpy_plpymodule.c:489 plpy_plpymodule.c:495
+#, c-format
+msgid "invalid SQLSTATE code"
+msgstr "código SQLSTATE no válido"
+
+#: plpy_procedure.c:225
+#, c-format
+msgid "trigger functions can only be called as triggers"
+msgstr "las funciones disparadoras sólo pueden ser llamadas como disparadores"
+
+#: plpy_procedure.c:229
+#, c-format
+msgid "PL/Python functions cannot return type %s"
+msgstr "las funciones PL/Python no pueden retornar el tipo %s"
+
+#: plpy_procedure.c:307
+#, c-format
+msgid "PL/Python functions cannot accept type %s"
+msgstr "la funciones PL/Python no pueden aceptar el tipo %s"
+
+#: plpy_procedure.c:395
+#, c-format
+msgid "could not compile PL/Python function \"%s\""
+msgstr "no se pudo compilar la función PL/Python «%s»"
+
+#: plpy_procedure.c:398
+#, c-format
+msgid "could not compile anonymous PL/Python code block"
+msgstr "no se pudo compilar el bloque anónimo PL/Python"
+
+#: plpy_resultobject.c:117 plpy_resultobject.c:143 plpy_resultobject.c:169
+#, c-format
+msgid "command did not produce a result set"
+msgstr "la orden no produjo un conjunto de resultados"
+
+#: plpy_spi.c:56
+#, c-format
+msgid "second argument of plpy.prepare must be a sequence"
+msgstr "el segundo argumento de plpy.prepare debe ser una secuencia"
+
+#: plpy_spi.c:98
+#, c-format
+msgid "plpy.prepare: type name at ordinal position %d is not a string"
+msgstr "plpy.prepare: el nombre de tipo en la posición %d no es una cadena"
+
+#: plpy_spi.c:170
+#, c-format
+msgid "plpy.execute expected a query or a plan"
+msgstr "plpy.execute espera una consulta o un plan"
+
+#: plpy_spi.c:189
+#, c-format
+msgid "plpy.execute takes a sequence as its second argument"
+msgstr "plpy.execute lleva una secuencia como segundo argumento"
+
+#: plpy_spi.c:297
+#, c-format
+msgid "SPI_execute_plan failed: %s"
+msgstr "falló SPI_execute_plan: %s"
+
+#: plpy_spi.c:339
+#, c-format
+msgid "SPI_execute failed: %s"
+msgstr "falló SPI_execute: %s"
+
+#: plpy_subxactobject.c:92
+#, c-format
+msgid "this subtransaction has already been entered"
+msgstr "ya se ha entrado en esta subtransacción"
+
+#: plpy_subxactobject.c:98 plpy_subxactobject.c:156
+#, c-format
+msgid "this subtransaction has already been exited"
+msgstr "ya se ha salido de esta subtransacción"
+
+#: plpy_subxactobject.c:150
+#, c-format
+msgid "this subtransaction has not been entered"
+msgstr "no se ha entrado en esta subtransacción"
+
+#: plpy_subxactobject.c:162
+#, c-format
+msgid "there is no subtransaction to exit from"
+msgstr "no hay una subtransacción de la cual salir"
+
+#: plpy_typeio.c:588
+#, c-format
+msgid "could not import a module for Decimal constructor"
+msgstr "no se pudo importar un módulo para el constructor Decimal"
+
+#: plpy_typeio.c:592
+#, c-format
+msgid "no Decimal attribute in module"
+msgstr "no se encontró atributo Decimal en el módulo"
+
+#: plpy_typeio.c:598
+#, c-format
+msgid "conversion from numeric to Decimal failed"
+msgstr "falló la conversión de numeric a Decimal"
+
+#: plpy_typeio.c:912
+#, c-format
+msgid "could not create bytes representation of Python object"
+msgstr "no se pudo crear la representación de cadena de bytes de Python"
+
+#: plpy_typeio.c:1049
+#, c-format
+msgid "could not create string representation of Python object"
+msgstr "no se pudo crear la representación de cadena de texto del objeto de Python"
+
+#: plpy_typeio.c:1060
+#, c-format
+msgid "could not convert Python object into cstring: Python string representation appears to contain null bytes"
+msgstr "no se pudo convertir el objeto Python a un cstring: la representación de cadena Python parece tener bytes nulos (\\0)"
+
+#: plpy_typeio.c:1157
+#, c-format
+msgid "return value of function with array return type is not a Python sequence"
+msgstr "el valor de retorno de la función con tipo de retorno array no es una secuencia Python"
+
+#: plpy_typeio.c:1202
+#, c-format
+msgid "could not determine sequence length for function return value"
+msgstr "no se pudo determinar el largo de secuencia del retorno de valor de la función"
+
+#: plpy_typeio.c:1222 plpy_typeio.c:1237 plpy_typeio.c:1253
+#, c-format
+msgid "multidimensional arrays must have array expressions with matching dimensions"
+msgstr "los arrays multidimensionales deben tener expresiones de arrays con dimensiones coincidentes"
+
+#: plpy_typeio.c:1227
+#, c-format
+msgid "number of array dimensions exceeds the maximum allowed (%d)"
+msgstr "el número de dimensiones del array excede el máximo permitido (%d)"
+
+#: plpy_typeio.c:1329
+#, c-format
+msgid "malformed record literal: \"%s\""
+msgstr "literal de record no es válido: «%s»"
+
+#: plpy_typeio.c:1330
+#, c-format
+msgid "Missing left parenthesis."
+msgstr "Falta paréntesis izquierdo."
+
+#: plpy_typeio.c:1331 plpy_typeio.c:1532
+#, c-format
+msgid "To return a composite type in an array, return the composite type as a Python tuple, e.g., \"[('foo',)]\"."
+msgstr "Para retornar un tipo compuesto en un array, retorne el tipo compuesto como una tupla de Python, e.g., «[('foo',)]»."
+
+#: plpy_typeio.c:1378
+#, c-format
+msgid "key \"%s\" not found in mapping"
+msgstr "la llave «%s» no fue encontrada en el mapa"
+
+#: plpy_typeio.c:1379
+#, c-format
+msgid "To return null in a column, add the value None to the mapping with the key named after the column."
+msgstr "Para retornar null en una columna, agregue el valor None al mapa, con llave llamada igual que la columna."
+
+#: plpy_typeio.c:1432
+#, c-format
+msgid "length of returned sequence did not match number of columns in row"
+msgstr "el tamaño de la secuencia retornada no concuerda con el número de columnas de la fila"
+
+#: plpy_typeio.c:1530
+#, c-format
+msgid "attribute \"%s\" does not exist in Python object"
+msgstr "el atributo «%s» no existe en el objeto Python"
+
+#: plpy_typeio.c:1533
+#, c-format
+msgid "To return null in a column, let the returned object have an attribute named after column with value None."
+msgstr "Para retornar null en una columna, haga que el objeto retornado tenga un atributo llamado igual que la columna, con valor None."
+
+#: plpy_util.c:31
+#, c-format
+msgid "could not convert Python Unicode object to bytes"
+msgstr "no se pudo convertir el objeto Unicode de Python a bytes"
+
+#: plpy_util.c:37
+#, c-format
+msgid "could not extract bytes from encoded string"
+msgstr "no se pudo extraer bytes desde la cadena codificada"
diff --git a/src/pl/plpython/po/fr.po b/src/pl/plpython/po/fr.po
new file mode 100644
index 0000000..2922a28
--- /dev/null
+++ b/src/pl/plpython/po/fr.po
@@ -0,0 +1,590 @@
+# LANGUAGE message translation file for plpython
+# Copyright (C) 2009-2022 PostgreSQL Global Development Group
+# This file is distributed under the same license as the plpython (PostgreSQL) package.
+#
+# Use these quotes: « %s »
+#
+# Guillaume Lelarge <guillaume@lelarge.info>, 2009-2022.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: PostgreSQL 15\n"
+"Report-Msgid-Bugs-To: pgsql-bugs@lists.postgresql.org\n"
+"POT-Creation-Date: 2023-07-30 09:08+0000\n"
+"PO-Revision-Date: 2022-04-12 17:29+0200\n"
+"Last-Translator: Guillaume Lelarge <guillaume@lelarge.info>\n"
+"Language-Team: French <guillaume@lelarge.info>\n"
+"Language: fr\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=(n > 1);\n"
+"X-Generator: Poedit 3.0.1\n"
+
+#: plpy_cursorobject.c:72
+#, c-format
+msgid "plpy.cursor expected a query or a plan"
+msgstr "plpy.cursor attendait une requête ou un plan"
+
+#: plpy_cursorobject.c:155
+#, c-format
+msgid "plpy.cursor takes a sequence as its second argument"
+msgstr "plpy.cursor prends une séquence dans son second argument"
+
+#: plpy_cursorobject.c:171 plpy_spi.c:205
+#, c-format
+msgid "could not execute plan"
+msgstr "n'a pas pu exécuter le plan"
+
+#: plpy_cursorobject.c:174 plpy_spi.c:208
+#, c-format
+msgid "Expected sequence of %d argument, got %d: %s"
+msgid_plural "Expected sequence of %d arguments, got %d: %s"
+msgstr[0] "Séquence attendue de %d argument, %d obtenu : %s"
+msgstr[1] "Séquence attendue de %d arguments, %d obtenus : %s"
+
+#: plpy_cursorobject.c:321
+#, c-format
+msgid "iterating a closed cursor"
+msgstr "itération d'un curseur fermé"
+
+#: plpy_cursorobject.c:329 plpy_cursorobject.c:395
+#, c-format
+msgid "iterating a cursor in an aborted subtransaction"
+msgstr "itération d'un curseur dans une sous-transaction annulée"
+
+#: plpy_cursorobject.c:387
+#, c-format
+msgid "fetch from a closed cursor"
+msgstr "récupérer à partir d'un curseur fermé"
+
+#: plpy_cursorobject.c:430 plpy_spi.c:401
+#, c-format
+msgid "query result has too many rows to fit in a Python list"
+msgstr "le résultat de la requête contient trop de lignes pour être intégré dans une liste Python"
+
+#: plpy_cursorobject.c:482
+#, c-format
+msgid "closing a cursor in an aborted subtransaction"
+msgstr "fermeture d'un curseur dans une sous-transaction annulée"
+
+#: plpy_elog.c:125 plpy_elog.c:126 plpy_plpymodule.c:530
+#, c-format
+msgid "%s"
+msgstr "%s"
+
+#: plpy_exec.c:139
+#, c-format
+msgid "unsupported set function return mode"
+msgstr "mode de retour non supporté pour la fonction SET"
+
+#: plpy_exec.c:140
+#, c-format
+msgid "PL/Python set-returning functions only support returning one value per call."
+msgstr ""
+"les fonctions PL/python renvoyant des ensembles supportent seulement une\n"
+"valeur renvoyée par appel."
+
+#: plpy_exec.c:153
+#, c-format
+msgid "returned object cannot be iterated"
+msgstr "l'objet renvoyé ne supporte pas les itérations"
+
+#: plpy_exec.c:154
+#, c-format
+msgid "PL/Python set-returning functions must return an iterable object."
+msgstr ""
+"les fonctions PL/python renvoyant des ensembles doivent renvoyer un objet\n"
+"itérable"
+
+#: plpy_exec.c:168
+#, c-format
+msgid "error fetching next item from iterator"
+msgstr "erreur lors de la récupération du prochain élément de l'itérateur"
+
+#: plpy_exec.c:211
+#, c-format
+msgid "PL/Python procedure did not return None"
+msgstr "la procédure PL/python n'a pas renvoyé None"
+
+#: plpy_exec.c:215
+#, c-format
+msgid "PL/Python function with return type \"void\" did not return None"
+msgstr "la fonction PL/python avec un code de retour « void » ne renvoyait pas None"
+
+#: plpy_exec.c:369 plpy_exec.c:393
+#, c-format
+msgid "unexpected return value from trigger procedure"
+msgstr "valeur de retour inattendue de la procédure trigger"
+
+#: plpy_exec.c:370
+#, c-format
+msgid "Expected None or a string."
+msgstr "Attendait None ou une chaîne de caractères."
+
+#: plpy_exec.c:383
+#, c-format
+msgid "PL/Python trigger function returned \"MODIFY\" in a DELETE trigger -- ignored"
+msgstr ""
+"la fonction trigger PL/python a renvoyé « MODIFY » dans un trigger DELETE\n"
+"-- ignoré"
+
+#: plpy_exec.c:394
+#, c-format
+msgid "Expected None, \"OK\", \"SKIP\", or \"MODIFY\"."
+msgstr "Attendait None, « OK », « SKIP » ou « MODIFY »."
+
+#: plpy_exec.c:444
+#, c-format
+msgid "PyList_SetItem() failed, while setting up arguments"
+msgstr "échec de PyList_SetItem() lors de l'initialisation des arguments"
+
+#: plpy_exec.c:448
+#, c-format
+msgid "PyDict_SetItemString() failed, while setting up arguments"
+msgstr "échec de PyDict_SetItemString() lors de l'initialisation des arguments"
+
+#: plpy_exec.c:460
+#, c-format
+msgid "function returning record called in context that cannot accept type record"
+msgstr ""
+"fonction renvoyant le type record appelée dans un contexte qui ne peut pas\n"
+"accepter le type record"
+
+#: plpy_exec.c:677
+#, c-format
+msgid "while creating return value"
+msgstr "lors de la création de la valeur de retour"
+
+#: plpy_exec.c:924
+#, c-format
+msgid "TD[\"new\"] deleted, cannot modify row"
+msgstr "TD[\"new\"] supprimé, ne peut pas modifier la ligne"
+
+#: plpy_exec.c:929
+#, c-format
+msgid "TD[\"new\"] is not a dictionary"
+msgstr "TD[\"new\"] n'est pas un dictionnaire"
+
+#: plpy_exec.c:954
+#, c-format
+msgid "TD[\"new\"] dictionary key at ordinal position %d is not a string"
+msgstr "la clé TD[\"new\"] à la position ordinale %d n'est pas une chaîne"
+
+#: plpy_exec.c:961
+#, c-format
+msgid "key \"%s\" found in TD[\"new\"] does not exist as a column in the triggering row"
+msgstr ""
+"la clé « %s » trouvée dans TD[\"new\"] n'existe pas comme colonne\n"
+"de la ligne impactée par le trigger"
+
+#: plpy_exec.c:966
+#, c-format
+msgid "cannot set system attribute \"%s\""
+msgstr "ne peut pas initialiser l'attribut système « %s »"
+
+#: plpy_exec.c:971
+#, c-format
+msgid "cannot set generated column \"%s\""
+msgstr "ne peut pas initialiser la colonne générée « %s »"
+
+#: plpy_exec.c:1029
+#, c-format
+msgid "while modifying trigger row"
+msgstr "lors de la modification de la ligne du trigger"
+
+#: plpy_exec.c:1087
+#, c-format
+msgid "forcibly aborting a subtransaction that has not been exited"
+msgstr "annulation forcée d'une sous-transaction qui n'a jamais été quittée"
+
+#: plpy_main.c:109
+#, c-format
+msgid "multiple Python libraries are present in session"
+msgstr "plusieurs bibliothèques Python sont présentes dans la session"
+
+#: plpy_main.c:110
+#, c-format
+msgid "Only one Python major version can be used in one session."
+msgstr "Seule une version majeure de Python peut être utilisée dans une session."
+
+#: plpy_main.c:122
+#, c-format
+msgid "untrapped error in initialization"
+msgstr "erreur non récupérée dans l'initialisation"
+
+#: plpy_main.c:145
+#, c-format
+msgid "could not import \"__main__\" module"
+msgstr "n'a pas pu importer le module « __main__ »"
+
+#: plpy_main.c:154
+#, c-format
+msgid "could not initialize globals"
+msgstr "n'a pas pu initialiser les variables globales"
+
+#: plpy_main.c:352
+#, c-format
+msgid "PL/Python procedure \"%s\""
+msgstr "procédure PL/python « %s »"
+
+#: plpy_main.c:355
+#, c-format
+msgid "PL/Python function \"%s\""
+msgstr "fonction PL/python « %s »"
+
+#: plpy_main.c:363
+#, c-format
+msgid "PL/Python anonymous code block"
+msgstr "bloc de code PL/Python anonyme"
+
+#: plpy_plpymodule.c:168 plpy_plpymodule.c:171
+#, c-format
+msgid "could not import \"plpy\" module"
+msgstr "n'a pas pu importer le module « plpy »"
+
+#: plpy_plpymodule.c:182
+#, c-format
+msgid "could not create the spiexceptions module"
+msgstr "n'a pas pu créer le module « spiexceptions »"
+
+#: plpy_plpymodule.c:190
+#, c-format
+msgid "could not add the spiexceptions module"
+msgstr "n'a pas pu ajouter le module « spiexceptions »"
+
+#: plpy_plpymodule.c:257
+#, c-format
+msgid "could not generate SPI exceptions"
+msgstr "n'a pas pu générer les exceptions SPI"
+
+#: plpy_plpymodule.c:425
+#, c-format
+msgid "could not unpack arguments in plpy.elog"
+msgstr "n'a pas pu déballer les arguments dans plpy.elog"
+
+#: plpy_plpymodule.c:434
+msgid "could not parse error message in plpy.elog"
+msgstr "n'a pas pu analyser le message d'erreur dans plpy.elog"
+
+#: plpy_plpymodule.c:451
+#, c-format
+msgid "argument 'message' given by name and position"
+msgstr "argument 'message' donné par nom et position"
+
+#: plpy_plpymodule.c:478
+#, c-format
+msgid "'%s' is an invalid keyword argument for this function"
+msgstr "'%s' est une argument mot-clé invalide pour cette fonction"
+
+#: plpy_plpymodule.c:489 plpy_plpymodule.c:495
+#, c-format
+msgid "invalid SQLSTATE code"
+msgstr "code SQLSTATE invalide"
+
+#: plpy_procedure.c:225
+#, c-format
+msgid "trigger functions can only be called as triggers"
+msgstr "les fonctions trigger peuvent seulement être appelées par des triggers"
+
+#: plpy_procedure.c:229
+#, c-format
+msgid "PL/Python functions cannot return type %s"
+msgstr "les fonctions PL/python ne peuvent pas renvoyer le type %s"
+
+#: plpy_procedure.c:307
+#, c-format
+msgid "PL/Python functions cannot accept type %s"
+msgstr "les fonctions PL/python ne peuvent pas accepter le type %s"
+
+#: plpy_procedure.c:395
+#, c-format
+msgid "could not compile PL/Python function \"%s\""
+msgstr "n'a pas pu compiler la fonction PL/python « %s »"
+
+#: plpy_procedure.c:398
+#, c-format
+msgid "could not compile anonymous PL/Python code block"
+msgstr "n'a pas pu compiler le bloc de code anonyme PL/python"
+
+#: plpy_resultobject.c:117 plpy_resultobject.c:143 plpy_resultobject.c:169
+#, c-format
+msgid "command did not produce a result set"
+msgstr "la commande n'a pas fourni d'ensemble de résultats"
+
+#: plpy_spi.c:56
+#, c-format
+msgid "second argument of plpy.prepare must be a sequence"
+msgstr "le second argument de plpy.prepare doit être une séquence"
+
+#: plpy_spi.c:98
+#, c-format
+msgid "plpy.prepare: type name at ordinal position %d is not a string"
+msgstr "plpy.prepare : le nom du type sur la position ordinale %d n'est pas une chaîne"
+
+#: plpy_spi.c:170
+#, c-format
+msgid "plpy.execute expected a query or a plan"
+msgstr "plpy.prepare attendait une requête ou un plan"
+
+#: plpy_spi.c:189
+#, c-format
+msgid "plpy.execute takes a sequence as its second argument"
+msgstr "plpy.execute prends une séquence dans son second argument"
+
+#: plpy_spi.c:297
+#, c-format
+msgid "SPI_execute_plan failed: %s"
+msgstr "échec de SPI_execute_plan : %s"
+
+#: plpy_spi.c:339
+#, c-format
+msgid "SPI_execute failed: %s"
+msgstr "échec de SPI_execute : %s"
+
+#: plpy_subxactobject.c:92
+#, c-format
+msgid "this subtransaction has already been entered"
+msgstr "cette sous-transaction est en cours d'utilisation"
+
+#: plpy_subxactobject.c:98 plpy_subxactobject.c:156
+#, c-format
+msgid "this subtransaction has already been exited"
+msgstr "déjà sorti de cette sous-transaction"
+
+#: plpy_subxactobject.c:150
+#, c-format
+msgid "this subtransaction has not been entered"
+msgstr "cette sous-transaction n'a jamais été utilisée"
+
+#: plpy_subxactobject.c:162
+#, c-format
+msgid "there is no subtransaction to exit from"
+msgstr "il n'y a pas de transaction à quitter"
+
+#: plpy_typeio.c:588
+#, c-format
+msgid "could not import a module for Decimal constructor"
+msgstr "n'a pas pu importer un module pour le constructeur Decimal"
+
+#: plpy_typeio.c:592
+#, c-format
+msgid "no Decimal attribute in module"
+msgstr "pas d'attribut Decimal dans le module"
+
+#: plpy_typeio.c:598
+#, c-format
+msgid "conversion from numeric to Decimal failed"
+msgstr "échec de la conversion numeric vers Decimal"
+
+#: plpy_typeio.c:912
+#, c-format
+msgid "could not create bytes representation of Python object"
+msgstr "n'a pas pu créer une représentation octets de l'objet Python"
+
+#: plpy_typeio.c:1049
+#, c-format
+msgid "could not create string representation of Python object"
+msgstr "n'a pas pu créer une représentation chaîne de caractères de l'objet Python"
+
+#: plpy_typeio.c:1060
+#, c-format
+msgid "could not convert Python object into cstring: Python string representation appears to contain null bytes"
+msgstr "n'a pas pu convertir l'objet Python en csting : la représentation de la chaîne Python contient des octets nuls"
+
+#: plpy_typeio.c:1157
+#, c-format
+msgid "return value of function with array return type is not a Python sequence"
+msgstr "la valeur de retour de la fonction de type tableau n'est pas une séquence Python"
+
+#: plpy_typeio.c:1202
+#, c-format
+msgid "could not determine sequence length for function return value"
+msgstr "n'a pas pu déterminer la longueur de la séquence pour la valeur de retour de la fonction"
+
+#: plpy_typeio.c:1222 plpy_typeio.c:1237 plpy_typeio.c:1253
+#, c-format
+msgid "multidimensional arrays must have array expressions with matching dimensions"
+msgstr ""
+"les tableaux multidimensionnels doivent avoir des expressions de tableaux\n"
+"avec les dimensions correspondantes"
+
+#: plpy_typeio.c:1227
+#, c-format
+msgid "number of array dimensions exceeds the maximum allowed (%d)"
+msgstr "le nombre de dimensions du tableau dépasse le maximum autorisé (%d)"
+
+#: plpy_typeio.c:1329
+#, c-format
+msgid "malformed record literal: \"%s\""
+msgstr "enregistrement litéral invalide : « %s »"
+
+#: plpy_typeio.c:1330
+#, c-format
+msgid "Missing left parenthesis."
+msgstr "Parenthèse gauche manquante."
+
+#: plpy_typeio.c:1331 plpy_typeio.c:1532
+#, c-format
+msgid "To return a composite type in an array, return the composite type as a Python tuple, e.g., \"[('foo',)]\"."
+msgstr "Pour renvoyer un type composite dans un tableau, renvoyez le type composite sous la forme d'un tuple Python, c'est-à-dire \"[('foo',)]\"."
+
+#: plpy_typeio.c:1378
+#, c-format
+msgid "key \"%s\" not found in mapping"
+msgstr "la clé « %s » introuvable dans la correspondance"
+
+#: plpy_typeio.c:1379
+#, c-format
+msgid "To return null in a column, add the value None to the mapping with the key named after the column."
+msgstr ""
+"Pour renvoyer NULL dans une colonne, ajoutez la valeur None à la\n"
+"correspondance de la clé nommée d'après la colonne."
+
+#: plpy_typeio.c:1432
+#, c-format
+msgid "length of returned sequence did not match number of columns in row"
+msgstr ""
+"la longueur de la séquence renvoyée ne correspondait pas au nombre de\n"
+"colonnes dans la ligne"
+
+#: plpy_typeio.c:1530
+#, c-format
+msgid "attribute \"%s\" does not exist in Python object"
+msgstr "l'attribut « %s » n'existe pas dans l'objet Python"
+
+#: plpy_typeio.c:1533
+#, c-format
+msgid "To return null in a column, let the returned object have an attribute named after column with value None."
+msgstr ""
+"Pour renvoyer NULL dans une colonne, faites en sorte que l'objet renvoyé ait\n"
+"un attribut nommé suivant la colonne de valeur None."
+
+#: plpy_util.c:31
+#, c-format
+msgid "could not convert Python Unicode object to bytes"
+msgstr "n'a pas pu convertir l'objet Unicode Python en octets"
+
+#: plpy_util.c:37
+#, c-format
+msgid "could not extract bytes from encoded string"
+msgstr "n'a pas pu extraire les octets de la chaîne encodée"
+
+#~ msgid "PL/Python does not support conversion to arrays of row types."
+#~ msgstr "PL/Python ne supporte pas les conversions vers des tableaux de types row."
+
+#~ msgid "PL/Python function \"%s\" could not execute plan"
+#~ msgstr "la fonction PL/python « %s » n'a pas pu exécuter un plan"
+
+#~ msgid "PL/Python function \"%s\" failed"
+#~ msgstr "échec de la fonction PL/python « %s »"
+
+#~ msgid "PL/Python only supports one-dimensional arrays."
+#~ msgstr "PL/Python supporte seulement les tableaux uni-dimensionnels."
+
+#~ msgid "PL/Python: %s"
+#~ msgstr "PL/python : %s"
+
+#~ msgid "PyCObject_AsVoidPtr() failed"
+#~ msgstr "échec de PyCObject_AsVoidPtr()"
+
+#~ msgid "PyCObject_FromVoidPtr() failed"
+#~ msgstr "échec de PyCObject_FromVoidPtr()"
+
+#~ msgid "Python major version mismatch in session"
+#~ msgstr "Différence de version majeure de Python dans la session"
+
+#~ msgid "Start a new session to use a different Python major version."
+#~ msgstr ""
+#~ "Lancez une nouvelle session pour utiliser une version majeure différente de\n"
+#~ "Python."
+
+#~ msgid "This session has previously used Python major version %d, and it is now attempting to use Python major version %d."
+#~ msgstr ""
+#~ "Cette session a auparavant utilisé la version majeure %d de Python et elle\n"
+#~ "essaie maintenant d'utiliser la version majeure %d."
+
+#, c-format
+#~ msgid "To construct a multidimensional array, the inner sequences must all have the same length."
+#~ msgstr "Pour construire un tableau multidimensionnel, les séquences internes doivent toutes avoir la même longueur."
+
+#, c-format
+#~ msgid "array size exceeds the maximum allowed"
+#~ msgstr "la taille du tableau dépasse le maximum permis"
+
+#~ msgid "cannot convert multidimensional array to Python list"
+#~ msgstr "ne peut pas convertir un tableau multidimensionnel en liste Python"
+
+#~ msgid "could not compute string representation of Python object in PL/Python function \"%s\" while modifying trigger row"
+#~ msgstr ""
+#~ "n'a pas pu traiter la représentation de la chaîne d'un objet Python dans\n"
+#~ "la fonction PL/Python « %s » lors de la modification de la ligne du trigger"
+
+#~ msgid "could not create exception \"%s\""
+#~ msgstr "n'a pas pu créer l'exception « %s »"
+
+#~ msgid "could not create globals"
+#~ msgstr "n'a pas pu créer les globales"
+
+#~ msgid "could not create new Python list"
+#~ msgstr "n'a pas pu créer la nouvelle liste Python"
+
+#~ msgid "could not create new dictionary"
+#~ msgstr "n'a pas pu créer le nouveau dictionnaire"
+
+#~ msgid "could not create new dictionary while building trigger arguments"
+#~ msgstr ""
+#~ "n'a pas pu créer un nouveau dictionnaire lors de la construction des\n"
+#~ "arguments du trigger"
+
+#~ msgid "could not create procedure cache"
+#~ msgstr "n'a pas pu créer le cache de procédure"
+
+#~ msgid "could not create string representation of Python object in PL/Python function \"%s\" while creating return value"
+#~ msgstr ""
+#~ "n'a pas pu créer la représentation en chaîne de caractère de l'objet\n"
+#~ "Python dans la fonction PL/python « %s » lors de la création de la valeur\n"
+#~ "de retour"
+
+#~ msgid "could not create the base SPI exceptions"
+#~ msgstr "n'a pas pu créer les exceptions SPI de base"
+
+#~ msgid "invalid arguments for plpy.prepare"
+#~ msgstr "arguments invalides pour plpy.prepare"
+
+#~ msgid "multidimensional arrays must have array expressions with matching dimensions. PL/Python function return value has sequence length %d while expected %d"
+#~ msgstr ""
+#~ "les tableaux multidimensionnels doivent avoir des expressions de tableaux\n"
+#~ "avec des dimensions correspondantes. La valeur de retour de la fonction\n"
+#~ "PL/Python a une longueur de séquence %d alors que %d est attendue"
+
+#~ msgid "out of memory"
+#~ msgstr "mémoire épuisée"
+
+#~ msgid "plan.status takes no arguments"
+#~ msgstr "plan.status ne prends pas d'arguments"
+
+#~ msgid "plpy.prepare does not support composite types"
+#~ msgstr "plpy.prepare ne supporte pas les types composites"
+
+#~ msgid "the message is already specified"
+#~ msgstr "le message est déjà spécifié"
+
+#~ msgid "transaction aborted"
+#~ msgstr "transaction annulée"
+
+#~ msgid "unrecognized error in PLy_spi_execute_fetch_result"
+#~ msgstr "erreur inconnue dans PLy_spi_execute_fetch_result"
+
+#~ msgid "unrecognized error in PLy_spi_execute_plan"
+#~ msgstr "erreur inconnue dans PLy_spi_execute_plan"
+
+#~ msgid "unrecognized error in PLy_spi_execute_query"
+#~ msgstr "erreur inconnue dans PLy_spi_execute_query"
+
+#~ msgid "unrecognized error in PLy_spi_prepare"
+#~ msgstr "erreur inconnue dans PLy_spi_prepare"
+
+#, c-format
+#~ msgid "wrong length of inner sequence: has length %d, but %d was expected"
+#~ msgstr "mauvaise longueur de la séquence interne : a une longueur %d, mais %d était attendu"
diff --git a/src/pl/plpython/po/it.po b/src/pl/plpython/po/it.po
new file mode 100644
index 0000000..26ef9e0
--- /dev/null
+++ b/src/pl/plpython/po/it.po
@@ -0,0 +1,470 @@
+#
+# plpython.po
+# Italian message translation file for plpython
+#
+# For development and bug report please use:
+# https://github.com/dvarrazzo/postgresql-it
+#
+# Copyright (C) 2012-2017 PostgreSQL Global Development Group
+# Copyright (C) 2010, Associazione Culturale ITPUG
+#
+# Daniele Varrazzo <daniele.varrazzo@gmail.com>, 2012-2017.
+# Flavio Spada <f.spada@sbv.mi.it>
+#
+# This file is distributed under the same license as the PostgreSQL package.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: plpython (PostgreSQL) 11\n"
+"Report-Msgid-Bugs-To: pgsql-bugs@lists.postgresql.org\n"
+"POT-Creation-Date: 2022-09-30 12:08+0000\n"
+"PO-Revision-Date: 2022-09-30 15:00+0200\n"
+"Last-Translator: Domenico Sgarbossa <sgarbossa.domenico@gmail.com>\n"
+"Language-Team: https://github.com/dvarrazzo/postgresql-it\n"
+"Language: it\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=n != 1;\n"
+"X-Generator: Poedit 2.3\n"
+
+#: plpy_cursorobject.c:72
+#, c-format
+msgid "plpy.cursor expected a query or a plan"
+msgstr "plpy.cursor richiede una query o un piano"
+
+#: plpy_cursorobject.c:155
+#, c-format
+msgid "plpy.cursor takes a sequence as its second argument"
+msgstr "plpy.cursor richiede una sequenza come secondo argomento"
+
+#: plpy_cursorobject.c:171 plpy_spi.c:205
+#, c-format
+msgid "could not execute plan"
+msgstr "esecuzione del piano fallita"
+
+#: plpy_cursorobject.c:174 plpy_spi.c:208
+#, c-format
+msgid "Expected sequence of %d argument, got %d: %s"
+msgid_plural "Expected sequence of %d arguments, got %d: %s"
+msgstr[0] "Attesa sequenza di %d argomento, ricevuti %d: %s"
+msgstr[1] "Attesa sequenza di %d argomenti, ricevuti %d: %s"
+
+#: plpy_cursorobject.c:321
+#, c-format
+msgid "iterating a closed cursor"
+msgstr "iterazione di un cursore chiuso"
+
+#: plpy_cursorobject.c:329 plpy_cursorobject.c:395
+#, c-format
+msgid "iterating a cursor in an aborted subtransaction"
+msgstr "iterazione di un cursore in una sotto-transazione interrotta"
+
+#: plpy_cursorobject.c:387
+#, c-format
+msgid "fetch from a closed cursor"
+msgstr "lettura da un cursore chiuso"
+
+#: plpy_cursorobject.c:430 plpy_spi.c:401
+#, c-format
+msgid "query result has too many rows to fit in a Python list"
+msgstr "il risultato della query ha troppe righe per una lista Python"
+
+#: plpy_cursorobject.c:482
+#, c-format
+msgid "closing a cursor in an aborted subtransaction"
+msgstr "chiusura di un cursore in una sotto-transazione interrotta"
+
+#: plpy_elog.c:125 plpy_elog.c:126 plpy_plpymodule.c:530
+#, c-format
+msgid "%s"
+msgstr "%s"
+
+#: plpy_exec.c:139
+#, c-format
+msgid "unsupported set function return mode"
+msgstr "modalità di ritorno della funzione set non supportata"
+
+#: plpy_exec.c:140
+#, c-format
+msgid "PL/Python set-returning functions only support returning one value per call."
+msgstr "Le funzioni PL/Python che restituiscono insiemi supportano la restituzione di un solo valore per chiamata."
+
+#: plpy_exec.c:153
+#, c-format
+msgid "returned object cannot be iterated"
+msgstr "l'oggetto restituito non può essere iterato"
+
+#: plpy_exec.c:154
+#, c-format
+msgid "PL/Python set-returning functions must return an iterable object."
+msgstr "Le funzioni PL/Python che restituiscono insiemi devono restituire un oggetto iterabile."
+
+#: plpy_exec.c:168
+#, c-format
+msgid "error fetching next item from iterator"
+msgstr "errore nell'ottenere l'elemento successivo dall'iteratore"
+
+#: plpy_exec.c:211
+#, c-format
+msgid "PL/Python procedure did not return None"
+msgstr "la procedura PL/Python non ha restituito None"
+
+#: plpy_exec.c:215
+#, c-format
+msgid "PL/Python function with return type \"void\" did not return None"
+msgstr "la funzione PL/Python che restituisce \"void\" non ha restituito None"
+
+#: plpy_exec.c:369 plpy_exec.c:395
+#, c-format
+msgid "unexpected return value from trigger procedure"
+msgstr "la prodedura trigger ha restituito un valore inatteso"
+
+#: plpy_exec.c:370
+#, c-format
+msgid "Expected None or a string."
+msgstr "Atteso None o una stringa."
+
+#: plpy_exec.c:385
+#, c-format
+msgid "PL/Python trigger function returned \"MODIFY\" in a DELETE trigger -- ignored"
+msgstr "la funzione trigger PL/Python ha restituito \"MODIFY\" in un trigger DELETE -- ignorato"
+
+#: plpy_exec.c:396
+#, c-format
+msgid "Expected None, \"OK\", \"SKIP\", or \"MODIFY\"."
+msgstr "Atteso None, \"OK\", \"SKIP\", o \"MODIFY\"."
+
+#: plpy_exec.c:441
+#, c-format
+msgid "PyList_SetItem() failed, while setting up arguments"
+msgstr "PyList_SetItem() è fallita durante l'impostazione degli argomenti"
+
+#: plpy_exec.c:445
+#, c-format
+msgid "PyDict_SetItemString() failed, while setting up arguments"
+msgstr "PyDict_SetItemString() è fallita durante l'impostazione degli argomenti"
+
+#: plpy_exec.c:457
+#, c-format
+msgid "function returning record called in context that cannot accept type record"
+msgstr "la funzione che restituisce un record è chiamata in un contesto che non può accettare il tipo record"
+
+#: plpy_exec.c:674
+#, c-format
+msgid "while creating return value"
+msgstr "durante la creazione del valore da restituire"
+
+#: plpy_exec.c:908
+#, c-format
+msgid "TD[\"new\"] deleted, cannot modify row"
+msgstr "TD[\"new\"] cancellato, non è possibile modificare la riga"
+
+#: plpy_exec.c:913
+#, c-format
+msgid "TD[\"new\"] is not a dictionary"
+msgstr "TD[\"new\"] non è un dizionario"
+
+#: plpy_exec.c:938
+#, c-format
+msgid "TD[\"new\"] dictionary key at ordinal position %d is not a string"
+msgstr "la chiave di dizionario TD[\"new\"] alla posizione %d non è una stringa"
+
+#: plpy_exec.c:945
+#, c-format
+msgid "key \"%s\" found in TD[\"new\"] does not exist as a column in the triggering row"
+msgstr "la chiave \"%s\" trovata in TD[\"new\"] non esiste come colonna nella riga del trigger"
+
+#: plpy_exec.c:950
+#, c-format
+msgid "cannot set system attribute \"%s\""
+msgstr "non è possibile impostare l'attributo di sistema \"%s\""
+
+#: plpy_exec.c:955
+#, c-format
+msgid "cannot set generated column \"%s\""
+msgstr "impossibile impostare la colonna generata \"%s\""
+
+#: plpy_exec.c:1013
+#, c-format
+msgid "while modifying trigger row"
+msgstr "durante la modifica della riga trigger"
+
+#: plpy_exec.c:1071
+#, c-format
+msgid "forcibly aborting a subtransaction that has not been exited"
+msgstr "interruzione forzata di una sotto-transazione che non è terminata"
+
+#: plpy_main.c:111
+#, c-format
+msgid "multiple Python libraries are present in session"
+msgstr "c'è più di una libreria Python presente nella sessione"
+
+#: plpy_main.c:112
+#, c-format
+msgid "Only one Python major version can be used in one session."
+msgstr "Solo una versione maggiore di Python può essere usata in una sessione."
+
+#: plpy_main.c:124
+#, c-format
+msgid "untrapped error in initialization"
+msgstr "errore non catturato durante l'inizializzazione"
+
+#: plpy_main.c:147
+#, c-format
+msgid "could not import \"__main__\" module"
+msgstr "importazione del modulo \"__main__\""
+
+#: plpy_main.c:156
+#, c-format
+msgid "could not initialize globals"
+msgstr "inizializzazione delle variabili globali fallita"
+
+#: plpy_main.c:354
+#, c-format
+msgid "PL/Python procedure \"%s\""
+msgstr "procedura PL/Python \"%s\""
+
+#: plpy_main.c:357
+#, c-format
+msgid "PL/Python function \"%s\""
+msgstr "funzione PL/Python \"%s\""
+
+#: plpy_main.c:365
+#, c-format
+msgid "PL/Python anonymous code block"
+msgstr "blocco di codice anonimo in PL/Python"
+
+#: plpy_plpymodule.c:168 plpy_plpymodule.c:171
+#, c-format
+msgid "could not import \"plpy\" module"
+msgstr "importazione del modulo \"plpy\" fallita"
+
+#: plpy_plpymodule.c:182
+#, c-format
+msgid "could not create the spiexceptions module"
+msgstr "creazione del modulo spiexceptions fallita"
+
+#: plpy_plpymodule.c:190
+#, c-format
+msgid "could not add the spiexceptions module"
+msgstr "aggiunta del modulo spiexceptions fallita"
+
+#: plpy_plpymodule.c:257
+#, c-format
+msgid "could not generate SPI exceptions"
+msgstr "generazione delle eccezioni SPI fallita"
+
+#: plpy_plpymodule.c:425
+#, c-format
+msgid "could not unpack arguments in plpy.elog"
+msgstr "non è stato possibile espandere gli argomenti in plpy.elog"
+
+#: plpy_plpymodule.c:434
+msgid "could not parse error message in plpy.elog"
+msgstr "non è stato possibile interpretare il messaggio di errore in plpy.elog"
+
+#: plpy_plpymodule.c:451
+#, c-format
+msgid "argument 'message' given by name and position"
+msgstr "parametro 'message' dato con nome e posizione"
+
+#: plpy_plpymodule.c:478
+#, c-format
+msgid "'%s' is an invalid keyword argument for this function"
+msgstr "'%s' è un nome di argomento non valido per questa funzione"
+
+#: plpy_plpymodule.c:489 plpy_plpymodule.c:495
+#, c-format
+msgid "invalid SQLSTATE code"
+msgstr "codice SQLSTATE non valido"
+
+#: plpy_procedure.c:225
+#, c-format
+msgid "trigger functions can only be called as triggers"
+msgstr "le funzioni trigger possono essere chiamate esclusivamente da trigger"
+
+#: plpy_procedure.c:229
+#, c-format
+msgid "PL/Python functions cannot return type %s"
+msgstr "le funzioni PL/Python non possono restituire il tipo %s"
+
+#: plpy_procedure.c:307
+#, c-format
+msgid "PL/Python functions cannot accept type %s"
+msgstr "le funzioni PL/Python non possono accettare il tipo %s"
+
+#: plpy_procedure.c:397
+#, c-format
+msgid "could not compile PL/Python function \"%s\""
+msgstr "compilazione della funzione PL/Python \"%s\" fallita"
+
+#: plpy_procedure.c:400
+#, c-format
+msgid "could not compile anonymous PL/Python code block"
+msgstr "compilazione del blocco di codice anonimo PL/Python fallita"
+
+#: plpy_resultobject.c:117 plpy_resultobject.c:143 plpy_resultobject.c:169
+#, c-format
+msgid "command did not produce a result set"
+msgstr "il comando non ha prodotto risultati"
+
+#: plpy_spi.c:56
+#, c-format
+msgid "second argument of plpy.prepare must be a sequence"
+msgstr "il secondo argomento di plpy.prepare deve essere una sequenza"
+
+#: plpy_spi.c:98
+#, c-format
+msgid "plpy.prepare: type name at ordinal position %d is not a string"
+msgstr "plpy.prepare: il nome del tipo nella posizione %d non è una stringa"
+
+#: plpy_spi.c:170
+#, c-format
+msgid "plpy.execute expected a query or a plan"
+msgstr "plpy.execute si aspetta una query o un plan"
+
+#: plpy_spi.c:189
+#, c-format
+msgid "plpy.execute takes a sequence as its second argument"
+msgstr "plpy.execute richiede una sequenza come secondo argomento"
+
+#: plpy_spi.c:297
+#, c-format
+msgid "SPI_execute_plan failed: %s"
+msgstr "SPI_execute_plan ha fallito: %s"
+
+#: plpy_spi.c:339
+#, c-format
+msgid "SPI_execute failed: %s"
+msgstr "SPI_execute ha fallito: %s"
+
+#: plpy_subxactobject.c:92
+#, c-format
+msgid "this subtransaction has already been entered"
+msgstr "si è già entrati in questa sotto-transazione"
+
+#: plpy_subxactobject.c:98 plpy_subxactobject.c:156
+#, c-format
+msgid "this subtransaction has already been exited"
+msgstr "si è già usciti da questa sotto-transazione"
+
+#: plpy_subxactobject.c:150
+#, c-format
+msgid "this subtransaction has not been entered"
+msgstr "non si è entrati in questa sotto-transazione"
+
+#: plpy_subxactobject.c:162
+#, c-format
+msgid "there is no subtransaction to exit from"
+msgstr "non c'è nessuna transazione da cui uscire"
+
+#: plpy_typeio.c:587
+#, c-format
+msgid "could not import a module for Decimal constructor"
+msgstr "importazione di un modulo per il costrutto Decimal fallita"
+
+#: plpy_typeio.c:591
+#, c-format
+msgid "no Decimal attribute in module"
+msgstr "attributo Decimal non trovato nel modulo"
+
+#: plpy_typeio.c:597
+#, c-format
+msgid "conversion from numeric to Decimal failed"
+msgstr "conversione da numeric a Decimal fallita"
+
+#: plpy_typeio.c:911
+#, c-format
+msgid "could not create bytes representation of Python object"
+msgstr "creazione della rappresentazione in byte dell'oggetto Python fallita"
+
+#: plpy_typeio.c:1048
+#, c-format
+msgid "could not create string representation of Python object"
+msgstr "creazione della rappresentazione stringa dell'oggetto Python fallita"
+
+#: plpy_typeio.c:1059
+#, c-format
+msgid "could not convert Python object into cstring: Python string representation appears to contain null bytes"
+msgstr "conversione dell'oggetto Python in cstring fallita: la rappresentazione stringa Python sembra contenere byte null"
+
+#: plpy_typeio.c:1170
+#, c-format
+msgid "number of array dimensions exceeds the maximum allowed (%d)"
+msgstr "il numero di dimensioni dell'array supera il massimo consentito (%d)"
+
+#: plpy_typeio.c:1175
+#, c-format
+msgid "could not determine sequence length for function return value"
+msgstr "errore nel determinare la lunghezza della sequenza per il valore di ritorno della funzione"
+
+#: plpy_typeio.c:1180 plpy_typeio.c:1186
+#, c-format
+msgid "array size exceeds the maximum allowed"
+msgstr "la dimensione dell'array supera il massimo consentito"
+
+#: plpy_typeio.c:1214
+#, c-format
+msgid "return value of function with array return type is not a Python sequence"
+msgstr "il valore restituito dalla funzione con tipo restituito array non è una sequenza Python"
+
+#: plpy_typeio.c:1261
+#, c-format
+msgid "wrong length of inner sequence: has length %d, but %d was expected"
+msgstr "lunghezza errata della sequenza interna: la lunghezza è %d ma era atteso %d"
+
+#: plpy_typeio.c:1263
+#, c-format
+msgid "To construct a multidimensional array, the inner sequences must all have the same length."
+msgstr "Per costruire un array multidimensionale le sequenze interne devono avere tutte la stessa lunghezza."
+
+#: plpy_typeio.c:1342
+#, c-format
+msgid "malformed record literal: \"%s\""
+msgstr "letterale di record non corretto: \"%s\""
+
+#: plpy_typeio.c:1343
+#, c-format
+msgid "Missing left parenthesis."
+msgstr "Parentesi aperta mancante."
+
+#: plpy_typeio.c:1344 plpy_typeio.c:1545
+#, c-format
+msgid "To return a composite type in an array, return the composite type as a Python tuple, e.g., \"[('foo',)]\"."
+msgstr "Per restutuire un tipo composito in un array, restituisci il tipo composito come tupla Python, per esempio \"[('foo',)]\" "
+
+#: plpy_typeio.c:1391
+#, c-format
+msgid "key \"%s\" not found in mapping"
+msgstr "la chiave \"%s\" non è stata trovata nel dizionario"
+
+#: plpy_typeio.c:1392
+#, c-format
+msgid "To return null in a column, add the value None to the mapping with the key named after the column."
+msgstr "Per restituire null in una colonna, inserire nella mappa il valore None con una chiave chiamata come la colonna."
+
+#: plpy_typeio.c:1445
+#, c-format
+msgid "length of returned sequence did not match number of columns in row"
+msgstr "la lunghezza della sequenza ritornata non rispetta il numero di colonne presenti nella riga"
+
+#: plpy_typeio.c:1543
+#, c-format
+msgid "attribute \"%s\" does not exist in Python object"
+msgstr "l'attributo \"%s\" non esiste nell'oggetto Python"
+
+#: plpy_typeio.c:1546
+#, c-format
+msgid "To return null in a column, let the returned object have an attribute named after column with value None."
+msgstr "Per restituire null in una colonna, l'oggetto restituito deve avere un attributo chiamato come la colonna con valore None."
+
+#: plpy_util.c:31
+#, c-format
+msgid "could not convert Python Unicode object to bytes"
+msgstr "conversione dell'oggetto Unicode Python in byte fallita"
+
+#: plpy_util.c:37
+#, c-format
+msgid "could not extract bytes from encoded string"
+msgstr "estrazione dei byte dalla stringa codificata fallita"
diff --git a/src/pl/plpython/po/ja.po b/src/pl/plpython/po/ja.po
new file mode 100644
index 0000000..5670478
--- /dev/null
+++ b/src/pl/plpython/po/ja.po
@@ -0,0 +1,497 @@
+# Japanese message translation file for plpython
+# Copyright (C) 2022 PostgreSQL Global Development Group
+# This file is distributed under the same license as the pg_archivecleanup (PostgreSQL) package.
+# Honda Shigehiro <honda@postgresql.jp>, 2012.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: plpython (PostgreSQL 15)\n"
+"Report-Msgid-Bugs-To: pgsql-bugs@lists.postgresql.org\n"
+"POT-Creation-Date: 2019-06-11 11:34+0900\n"
+"PO-Revision-Date: 2021-08-25 17:42+0900\n"
+"Last-Translator: Kyotaro Horiguchi <horikyota.ntt@gmail.com>\n"
+"Language-Team: Japan PostgreSQL Users Group <jpug-doc@ml.postgresql.jp>\n"
+"Language: ja\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=1; plural=0;\n"
+"X-Generator: Poedit 1.8.13\n"
+
+#: plpy_cursorobject.c:78
+#, c-format
+msgid "plpy.cursor expected a query or a plan"
+msgstr "plpy.cursor は問い合わせもしくは実行計画を期待していました"
+
+#: plpy_cursorobject.c:161
+#, c-format
+msgid "plpy.cursor takes a sequence as its second argument"
+msgstr "plpy.cursor は第二引数としてシーケンスを取ります"
+
+#: plpy_cursorobject.c:177 plpy_spi.c:211
+#, c-format
+msgid "could not execute plan"
+msgstr "実行計画を実行できませんでした"
+
+#: plpy_cursorobject.c:180 plpy_spi.c:214
+#, c-format
+msgid "Expected sequence of %d argument, got %d: %s"
+msgid_plural "Expected sequence of %d arguments, got %d: %s"
+msgstr[0] "%d 個の引数のシーケンスを期待していましたが、個数は %d でした:%s"
+
+#: plpy_cursorobject.c:329
+#, c-format
+msgid "iterating a closed cursor"
+msgstr "反復利用しようとしているカーソルは、すでにクローズされています"
+
+#: plpy_cursorobject.c:337 plpy_cursorobject.c:403
+#, c-format
+msgid "iterating a cursor in an aborted subtransaction"
+msgstr ""
+"中断されたサブトランザクションの中でカーソルを反復利用しようとしています"
+
+#: plpy_cursorobject.c:395
+#, c-format
+msgid "fetch from a closed cursor"
+msgstr "クローズされたカーソルからのフェッチ"
+
+#: plpy_cursorobject.c:438 plpy_spi.c:409
+#, c-format
+msgid "query result has too many rows to fit in a Python list"
+msgstr "問い合わせの結果に含まれる行数が、Pythonのリストに対して多すぎます"
+
+#: plpy_cursorobject.c:490
+#, c-format
+msgid "closing a cursor in an aborted subtransaction"
+msgstr ""
+"中断されたサブトランザクションの中でカーソルをクローズしようとしています"
+
+#: plpy_elog.c:129 plpy_elog.c:130 plpy_plpymodule.c:553
+#, c-format
+msgid "%s"
+msgstr "%s"
+
+#: plpy_exec.c:143
+#, c-format
+msgid "unsupported set function return mode"
+msgstr "非サポートの集合関数リターンモードです。"
+
+#: plpy_exec.c:144
+#, c-format
+msgid ""
+"PL/Python set-returning functions only support returning one value per call."
+msgstr ""
+"集合を返却するPL/Python関数では、1回の呼び出しに対して1つの値を返すことのみが"
+"サポートされています。"
+
+#: plpy_exec.c:157
+#, c-format
+msgid "returned object cannot be iterated"
+msgstr "返されたオブジェクトは反復利用できません"
+
+#: plpy_exec.c:158
+#, c-format
+msgid "PL/Python set-returning functions must return an iterable object."
+msgstr ""
+"PL/Pythonの集合を返す関数は、反復処理可能なオブジェクトを返さなければなりませ"
+"ん。"
+
+#: plpy_exec.c:172
+#, c-format
+msgid "error fetching next item from iterator"
+msgstr "反復子から次の項目を取り出せませんでした"
+
+#: plpy_exec.c:215
+#, c-format
+msgid "PL/Python procedure did not return None"
+msgstr "PL/Python プロシージャが None を返しませんでした"
+
+#: plpy_exec.c:219
+#, c-format
+msgid "PL/Python function with return type \"void\" did not return None"
+msgstr "戻り値が\"void\"型である PL/Python関数がNoneを返しませんでした"
+
+#: plpy_exec.c:375 plpy_exec.c:401
+#, c-format
+msgid "unexpected return value from trigger procedure"
+msgstr "トリガプロシージャから期待しない戻り値が返されました"
+
+#: plpy_exec.c:376
+#, c-format
+msgid "Expected None or a string."
+msgstr "None もしくは文字列を期待していました。"
+
+#: plpy_exec.c:391
+#, c-format
+msgid ""
+"PL/Python trigger function returned \"MODIFY\" in a DELETE trigger -- ignored"
+msgstr ""
+"PL/Python トリガ関数が、DELETE トリガで \"MODIFY\" を返しました-- 無視します"
+
+#: plpy_exec.c:402
+#, c-format
+msgid "Expected None, \"OK\", \"SKIP\", or \"MODIFY\"."
+msgstr "None, \"OK\", \"SKIP\", \"MODIFY\" のいずれかを期待していました。"
+
+#: plpy_exec.c:452
+#, c-format
+msgid "PyList_SetItem() failed, while setting up arguments"
+msgstr "引数を設定する際に、PyList_SetItem() に失敗しました"
+
+#: plpy_exec.c:456
+#, c-format
+msgid "PyDict_SetItemString() failed, while setting up arguments"
+msgstr "引数を設定する際に、PyDict_SetItemString() に失敗しました"
+
+#: plpy_exec.c:468
+#, c-format
+msgid ""
+"function returning record called in context that cannot accept type record"
+msgstr ""
+"レコード型を受け付けられないコンテキストでレコードを返す関数が呼び出されまし"
+"た"
+
+#: plpy_exec.c:685
+#, c-format
+msgid "while creating return value"
+msgstr "戻り値を生成する際に"
+
+#: plpy_exec.c:919
+#, c-format
+msgid "TD[\"new\"] deleted, cannot modify row"
+msgstr "TD[\"new\"] は削除されました。行を変更できません。"
+
+#: plpy_exec.c:924
+#, c-format
+msgid "TD[\"new\"] is not a dictionary"
+msgstr "TD[\"new\"] は辞書ではありません"
+
+#: plpy_exec.c:951
+#, c-format
+msgid "TD[\"new\"] dictionary key at ordinal position %d is not a string"
+msgstr "TD[\"new\"] 辞書の%d番目のキーが文字列ではありません"
+
+#: plpy_exec.c:958
+#, c-format
+msgid ""
+"key \"%s\" found in TD[\"new\"] does not exist as a column in the triggering "
+"row"
+msgstr ""
+"TD[\"new\"] で見つかったキー \"%s\" は、行レベルトリガにおけるカラムとしては"
+"存在しません"
+
+#: plpy_exec.c:963
+#, c-format
+msgid "cannot set system attribute \"%s\""
+msgstr "システム属性\"%s\"は設定できません"
+
+#: plpy_exec.c:968
+#, c-format
+#| msgid "cannot alter inherited column \"%s\""
+msgid "cannot set generated column \"%s\""
+msgstr "生成列\"%s\"は設定できません"
+
+#: plpy_exec.c:1026
+#, c-format
+msgid "while modifying trigger row"
+msgstr "トリガ行を変更する際に"
+
+#: plpy_exec.c:1087
+#, c-format
+msgid "forcibly aborting a subtransaction that has not been exited"
+msgstr "終了していないサブトランザクションを強制的にアボートしています"
+
+#: plpy_main.c:125
+#, c-format
+msgid "multiple Python libraries are present in session"
+msgstr "セッションに複数の Python ライブラリが存在します"
+
+#: plpy_main.c:126
+#, c-format
+msgid "Only one Python major version can be used in one session."
+msgstr "1個のセッション中で使えるPythonのメジャーバージョンは1種類だけです。"
+
+#: plpy_main.c:142
+#, c-format
+msgid "untrapped error in initialization"
+msgstr "初期化中に捕捉できないエラーがありました"
+
+#: plpy_main.c:165
+#, c-format
+msgid "could not import \"__main__\" module"
+msgstr "\"__main__\" モジュールをインポートできませんでした"
+
+#: plpy_main.c:174
+#, c-format
+msgid "could not initialize globals"
+msgstr "グローバル変数(globals)を初期化できませんでした"
+
+#: plpy_main.c:399
+#, c-format
+msgid "PL/Python procedure \"%s\""
+msgstr "PL/Pythonプロシージャ\"%s\""
+
+#: plpy_main.c:402
+#, c-format
+msgid "PL/Python function \"%s\""
+msgstr "PL/Python関数\"%s\""
+
+#: plpy_main.c:410
+#, c-format
+msgid "PL/Python anonymous code block"
+msgstr "PL/Pythonの無名コードブロック"
+
+#: plpy_plpymodule.c:186 plpy_plpymodule.c:189
+#, c-format
+msgid "could not import \"plpy\" module"
+msgstr "\"plpy\"モジュールをインポートできませんでした"
+
+#: plpy_plpymodule.c:204
+#, c-format
+msgid "could not create the spiexceptions module"
+msgstr "spiexceptionsモジュールを生成できませんでした"
+
+#: plpy_plpymodule.c:212
+#, c-format
+msgid "could not add the spiexceptions module"
+msgstr "spiexceptionsモジュールを追加できませんでした"
+
+#: plpy_plpymodule.c:280
+#, c-format
+msgid "could not generate SPI exceptions"
+msgstr "SPI例外を生成できませんでした"
+
+#: plpy_plpymodule.c:448
+#, c-format
+msgid "could not unpack arguments in plpy.elog"
+msgstr "plpy.elogで引数を展開できませんでした"
+
+#: plpy_plpymodule.c:457
+msgid "could not parse error message in plpy.elog"
+msgstr "plpy.elogでエラーメッセージをパースできませんでした"
+
+#: plpy_plpymodule.c:474
+#, c-format
+msgid "argument 'message' given by name and position"
+msgstr "名前と位置で 'message' 引数が渡されました"
+
+#: plpy_plpymodule.c:501
+#, c-format
+msgid "'%s' is an invalid keyword argument for this function"
+msgstr "この関数に対して'%s'は無効なキーワード引数です"
+
+#: plpy_plpymodule.c:512 plpy_plpymodule.c:518
+#, c-format
+msgid "invalid SQLSTATE code"
+msgstr "無効なSQLSTATEコードです"
+
+#: plpy_procedure.c:230
+#, c-format
+msgid "trigger functions can only be called as triggers"
+msgstr "トリガー関数はトリガーとしてのみ呼び出せます"
+
+#: plpy_procedure.c:234
+#, c-format
+msgid "PL/Python functions cannot return type %s"
+msgstr "PL/Python関数は%s型を返せません"
+
+#: plpy_procedure.c:312
+#, c-format
+msgid "PL/Python functions cannot accept type %s"
+msgstr "PL/Python関数は%s型を受け付けられません"
+
+#: plpy_procedure.c:402
+#, c-format
+msgid "could not compile PL/Python function \"%s\""
+msgstr "PL/Python関数\"%s\"をコンパイルできませんでした"
+
+#: plpy_procedure.c:405
+#, c-format
+msgid "could not compile anonymous PL/Python code block"
+msgstr "PL/Python無名コードブロックをコンパイルできませんでした"
+
+#: plpy_resultobject.c:121 plpy_resultobject.c:147 plpy_resultobject.c:173
+#, c-format
+msgid "command did not produce a result set"
+msgstr "コマンドは結果セットを生成しませんでした"
+
+#: plpy_spi.c:60
+#, c-format
+msgid "second argument of plpy.prepare must be a sequence"
+msgstr "plpy.prepareの第二引数はシーケンスでなければなりません"
+
+#: plpy_spi.c:104
+#, c-format
+msgid "plpy.prepare: type name at ordinal position %d is not a string"
+msgstr "plpy.prepare: %d 番目の型名が文字列ではありません"
+
+#: plpy_spi.c:176
+#, c-format
+msgid "plpy.execute expected a query or a plan"
+msgstr "plpy.execute は問い合わせもしくは実行計画を期待していました"
+
+#: plpy_spi.c:195
+#, c-format
+msgid "plpy.execute takes a sequence as its second argument"
+msgstr "plpy.execute は第二引数としてシーケンスを取ります"
+
+#: plpy_spi.c:305
+#, c-format
+msgid "SPI_execute_plan failed: %s"
+msgstr "SPI_execute_plan が失敗しました: %s"
+
+#: plpy_spi.c:347
+#, c-format
+msgid "SPI_execute failed: %s"
+msgstr "SPI_execute が失敗しました: %s"
+
+#: plpy_subxactobject.c:97
+#, c-format
+msgid "this subtransaction has already been entered"
+msgstr "すでにこのサブトランザクションの中に入っています"
+
+#: plpy_subxactobject.c:103 plpy_subxactobject.c:161
+#, c-format
+msgid "this subtransaction has already been exited"
+msgstr "このサブトランザクションからすでに抜けています"
+
+#: plpy_subxactobject.c:155
+#, c-format
+msgid "this subtransaction has not been entered"
+msgstr "このサブトランザクションには入っていません"
+
+#: plpy_subxactobject.c:167
+#, c-format
+msgid "there is no subtransaction to exit from"
+msgstr "抜けるべきサブトランザクションがありません"
+
+#: plpy_typeio.c:591
+#, c-format
+msgid "could not import a module for Decimal constructor"
+msgstr "Decimalコンストラクタのためのモジュールをインポートできませんでした"
+
+#: plpy_typeio.c:595
+#, c-format
+msgid "no Decimal attribute in module"
+msgstr "モジュールの中にDecimal属性が含まれていません"
+
+#: plpy_typeio.c:601
+#, c-format
+msgid "conversion from numeric to Decimal failed"
+msgstr "numericからDecimalへの変換に失敗しました"
+
+#: plpy_typeio.c:915
+#, c-format
+msgid "could not create bytes representation of Python object"
+msgstr "Pythonオブジェクトのバイト表現を生成できませんでした"
+
+#: plpy_typeio.c:1063
+#, c-format
+msgid "could not create string representation of Python object"
+msgstr "Pythonオブジェクトの文字列表現を生成できませんでした"
+
+#: plpy_typeio.c:1074
+#, c-format
+msgid ""
+"could not convert Python object into cstring: Python string representation "
+"appears to contain null bytes"
+msgstr ""
+"Pythonオブジェクトをcstringに変換できませんでした: Pythonの文字列表現にnullバ"
+"イトが含まれているようです"
+
+#: plpy_typeio.c:1183
+#, c-format
+msgid "number of array dimensions exceeds the maximum allowed (%d)"
+msgstr "配列の次元数が制限値(%d)を超えています"
+
+#: plpy_typeio.c:1187
+#, c-format
+msgid "could not determine sequence length for function return value"
+msgstr "関数の戻り値について、シーケンスの長さを決定できませんでした"
+
+#: plpy_typeio.c:1190 plpy_typeio.c:1194
+#, c-format
+msgid "array size exceeds the maximum allowed"
+msgstr "配列のサイズが制限値を超えています"
+
+#: plpy_typeio.c:1220
+#, c-format
+msgid ""
+"return value of function with array return type is not a Python sequence"
+msgstr "配列型を返す関数の戻り値がPythonのシーケンスではありません"
+
+#: plpy_typeio.c:1266
+#, c-format
+msgid "wrong length of inner sequence: has length %d, but %d was expected"
+msgstr "内部シーケンスで長さが異常です: 長さは%dですが、期待する値は%dでした"
+
+#: plpy_typeio.c:1268
+#, c-format
+msgid ""
+"To construct a multidimensional array, the inner sequences must all have the "
+"same length."
+msgstr ""
+"多次元配列を生成する場合、内部シーケンスはすべて同じ長さでなければなりませ"
+"ん。"
+
+#: plpy_typeio.c:1347
+#, c-format
+msgid "malformed record literal: \"%s\""
+msgstr "不正な形式のレコードリテラル: \"%s\""
+
+#: plpy_typeio.c:1348
+#, c-format
+msgid "Missing left parenthesis."
+msgstr "左括弧がありません。"
+
+#: plpy_typeio.c:1349 plpy_typeio.c:1550
+#, c-format
+msgid ""
+"To return a composite type in an array, return the composite type as a "
+"Python tuple, e.g., \"[('foo',)]\"."
+msgstr ""
+"複合型を配列に入れて返したい場合、 \"[('foo',)]\" のように複合型を Pythonのタ"
+"プルとして返すようにしてください。"
+
+#: plpy_typeio.c:1396
+#, c-format
+msgid "key \"%s\" not found in mapping"
+msgstr "マッピング上にキー\"%s\"が見つかりません"
+
+#: plpy_typeio.c:1397
+#, c-format
+msgid ""
+"To return null in a column, add the value None to the mapping with the key "
+"named after the column."
+msgstr ""
+"カラムにnullを入れて返す場合、カラム名をキーとして値がNoneのエントリをマッピ"
+"ングに追加してください。"
+
+#: plpy_typeio.c:1450
+#, c-format
+msgid "length of returned sequence did not match number of columns in row"
+msgstr "返されたシーケンスの長さが行のカラム数とマッチしませんでした"
+
+#: plpy_typeio.c:1548
+#, c-format
+msgid "attribute \"%s\" does not exist in Python object"
+msgstr "属性\"%s\"がPythonオブジェクト中に存在しません"
+
+#: plpy_typeio.c:1551
+#, c-format
+msgid ""
+"To return null in a column, let the returned object have an attribute named "
+"after column with value None."
+msgstr ""
+"カラムにnullを入れて返す場合、カラム名をキーとして値がNoneである属性を持つオ"
+"ブジェクトを返すようにしてください。"
+
+#: plpy_util.c:35
+#, c-format
+msgid "could not convert Python Unicode object to bytes"
+msgstr "PythonのUnicodeオブジェクトをバイト列に変換できませんでした"
+
+#: plpy_util.c:41
+#, c-format
+msgid "could not extract bytes from encoded string"
+msgstr "エンコードされた文字列からバイト列を抽出できませんでした"
diff --git a/src/pl/plpython/po/ka.po b/src/pl/plpython/po/ka.po
new file mode 100644
index 0000000..70a87f6
--- /dev/null
+++ b/src/pl/plpython/po/ka.po
@@ -0,0 +1,462 @@
+# Georgian message translation file for plpython
+# Copyright (C) 2022 PostgreSQL Global Development Group
+# This file is distributed under the same license as the plpython (PostgreSQL) package.
+# Temuri Doghonadze <temuri.doghonadze@gmail.com>, 2022.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: plpython (PostgreSQL) 15\n"
+"Report-Msgid-Bugs-To: pgsql-bugs@lists.postgresql.org\n"
+"POT-Creation-Date: 2023-05-17 01:38+0000\n"
+"PO-Revision-Date: 2022-09-25 19:18+0200\n"
+"Last-Translator: Temuri Doghonadze <temuri.doghonadze@gmail.com>\n"
+"Language-Team: Georgian <nothing>\n"
+"Language: ka\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+"X-Generator: Poedit 3.1.1\n"
+
+#: plpy_cursorobject.c:72
+#, c-format
+msgid "plpy.cursor expected a query or a plan"
+msgstr "plpy.cursor გეგმას ან მოთხოვნას მოელოდა"
+
+#: plpy_cursorobject.c:155
+#, c-format
+msgid "plpy.cursor takes a sequence as its second argument"
+msgstr "plpy.cursor მეორე არგუმენტად მიმდევრობას იღებს"
+
+#: plpy_cursorobject.c:171 plpy_spi.c:205
+#, c-format
+msgid "could not execute plan"
+msgstr "გეგმის შესრულება ვერ მოხერხდა"
+
+#: plpy_cursorobject.c:174 plpy_spi.c:208
+#, c-format
+msgid "Expected sequence of %d argument, got %d: %s"
+msgid_plural "Expected sequence of %d arguments, got %d: %s"
+msgstr[0] "მოველოდი %d არგუმენტის მიმდევრობას. მივიღე %d: %s"
+msgstr[1] "მოველოდი %d არგუმენტის მიმდევრობას. მივიღე %d: %s"
+
+#: plpy_cursorobject.c:321
+#, c-format
+msgid "iterating a closed cursor"
+msgstr "დახურული კურსორის იტერაცია"
+
+#: plpy_cursorobject.c:329 plpy_cursorobject.c:395
+#, c-format
+msgid "iterating a cursor in an aborted subtransaction"
+msgstr "კურსორის იტერაცია გაუქმებულ ტრანზაქციაში"
+
+#: plpy_cursorobject.c:387
+#, c-format
+msgid "fetch from a closed cursor"
+msgstr "დახურული კურსორიდან გამოთხოვა"
+
+#: plpy_cursorobject.c:430 plpy_spi.c:401
+#, c-format
+msgid "query result has too many rows to fit in a Python list"
+msgstr "მოთხოვნის პასუხს Python-ის სიაში ჩასატევად მეტისმეტად ბევრი მწკრივი გააჩნია"
+
+#: plpy_cursorobject.c:482
+#, c-format
+msgid "closing a cursor in an aborted subtransaction"
+msgstr "გაუქმებულ ქვეტრანზაქციაში კურსორის დახურვა"
+
+#: plpy_elog.c:125 plpy_elog.c:126 plpy_plpymodule.c:530
+#, c-format
+msgid "%s"
+msgstr "%s"
+
+#: plpy_exec.c:139
+#, c-format
+msgid "unsupported set function return mode"
+msgstr "სეტების დამბრუნებელი ფუნქციის მხარდაუჭერელი რეჟიმი"
+
+#: plpy_exec.c:140
+#, c-format
+msgid "PL/Python set-returning functions only support returning one value per call."
+msgstr "PL/Python -ის ფუნქციებს, რომლების სეტებს აბრუნებენ, თითოეულ გამოძახებაზე მხოლოდ ერთი მნიშვნელობის მხარდაჭერა გააჩნიათ."
+
+#: plpy_exec.c:153
+#, c-format
+msgid "returned object cannot be iterated"
+msgstr "დაბრუნებული ობიექტის იტერაცია შეუძლებელია"
+
+#: plpy_exec.c:154
+#, c-format
+msgid "PL/Python set-returning functions must return an iterable object."
+msgstr "PL/Python-ის ფუნქციებმა, რომლებიც სეტებს აბრუნებენ, იტერირებადი ობიექტი უნდა დააბრუნონ."
+
+#: plpy_exec.c:168
+#, c-format
+msgid "error fetching next item from iterator"
+msgstr "იტერატორიდან შემდეგი ჩანაწერის მოთხოვის შეცდომა"
+
+#: plpy_exec.c:211
+#, c-format
+msgid "PL/Python procedure did not return None"
+msgstr "PL/Python -ის პროცედურამ None არ დააბრუნა"
+
+#: plpy_exec.c:215
+#, c-format
+msgid "PL/Python function with return type \"void\" did not return None"
+msgstr "PL/Python -ის ფუნქციამ, რომელიც აბრუნებს ტიპს \"void\", რაღაც დააბრუნა"
+
+#: plpy_exec.c:369 plpy_exec.c:393
+#, c-format
+msgid "unexpected return value from trigger procedure"
+msgstr "ტრიგერის პროცედურის მოულოდნელი დაბრუნებული მნიშვნელობა"
+
+#: plpy_exec.c:370
+#, c-format
+msgid "Expected None or a string."
+msgstr "ველოდებოდი არაფერს ან სტრიქონს."
+
+#: plpy_exec.c:383
+#, c-format
+msgid "PL/Python trigger function returned \"MODIFY\" in a DELETE trigger -- ignored"
+msgstr "PL/Python -ის ტრიგერმა ფუნქციამ DELETE ტრიგერში \"MODIFY\" დააბრუნა. -- იგნორირებულია"
+
+#: plpy_exec.c:394
+#, c-format
+msgid "Expected None, \"OK\", \"SKIP\", or \"MODIFY\"."
+msgstr "მოსალოდნელია არცერთი, \"OK\", \"SKIP\", ან \"MODIFY\"."
+
+#: plpy_exec.c:444
+#, c-format
+msgid "PyList_SetItem() failed, while setting up arguments"
+msgstr "PyList_SetItem() -ის შეცდომა არგუმენტების მორგებისას"
+
+#: plpy_exec.c:448
+#, c-format
+msgid "PyDict_SetItemString() failed, while setting up arguments"
+msgstr "PyDict_SetItemString() -ის შეცდომა არგუმენტების მორგებისას"
+
+#: plpy_exec.c:460
+#, c-format
+msgid "function returning record called in context that cannot accept type record"
+msgstr "ფუნქცია, რომელიც ჩანაწერს აბრუნებს, გამოძახებულია კონტექსტში, რომელსაც ჩანაწერის მიღება არ შეუძლია"
+
+#: plpy_exec.c:677
+#, c-format
+msgid "while creating return value"
+msgstr "დასაბრუნებელი მნიშვნელობის შექმნისას"
+
+#: plpy_exec.c:924
+#, c-format
+msgid "TD[\"new\"] deleted, cannot modify row"
+msgstr "TD[\"new\"] წაშლილია, მწკრივის შეცვლა შეუძლებელია"
+
+#: plpy_exec.c:929
+#, c-format
+msgid "TD[\"new\"] is not a dictionary"
+msgstr "TD[\"new\"] ლექსიკონი არაა"
+
+#: plpy_exec.c:954
+#, c-format
+msgid "TD[\"new\"] dictionary key at ordinal position %d is not a string"
+msgstr "TD[\"new\"] ლექსიკონის გასაღები ორდინალურ მდებარეობაზე %d სტრიქონს არ წარმოადგენს"
+
+#: plpy_exec.c:961
+#, c-format
+msgid "key \"%s\" found in TD[\"new\"] does not exist as a column in the triggering row"
+msgstr "\"TD[\"new\"]-ში ნაპოვნი გასაღები (%s) დამატრიგერებელი მწკრივის სვეტად არ არსებობს"
+
+#: plpy_exec.c:966
+#, c-format
+msgid "cannot set system attribute \"%s\""
+msgstr "სისტემური ატრიბუტის დაყენების შეცდომა: \"%s\""
+
+#: plpy_exec.c:971
+#, c-format
+msgid "cannot set generated column \"%s\""
+msgstr "გენერირებული სვეტის დაყენება შეუძლებელია: %s"
+
+#: plpy_exec.c:1029
+#, c-format
+msgid "while modifying trigger row"
+msgstr "ტრიგერის მწკრივის შეცვლისას"
+
+#: plpy_exec.c:1087
+#, c-format
+msgid "forcibly aborting a subtransaction that has not been exited"
+msgstr "ქვეტრანზაქცია, რომელიც ჯერ არ დასრულებულა, ძალით დასრულდება"
+
+#: plpy_main.c:109
+#, c-format
+msgid "multiple Python libraries are present in session"
+msgstr "სესია Python-ის ერთზე მეტ ბიბლიოთეკას შეიცავს"
+
+#: plpy_main.c:110
+#, c-format
+msgid "Only one Python major version can be used in one session."
+msgstr "ერთ სესიაში Python-ის მხოლოდ ერთი ძირითადი ვერსია შეგიძლიათ გამოიყენოთ."
+
+#: plpy_main.c:122
+#, c-format
+msgid "untrapped error in initialization"
+msgstr "დაუჭერელი შეცდომა ინიციალიზაციისას"
+
+#: plpy_main.c:145
+#, c-format
+msgid "could not import \"__main__\" module"
+msgstr "\"__main__\" მოდულის შემოტანის შეცდომა"
+
+#: plpy_main.c:154
+#, c-format
+msgid "could not initialize globals"
+msgstr "გლობალების ინიციალიზაციის შეცდომა"
+
+#: plpy_main.c:352
+#, c-format
+msgid "PL/Python procedure \"%s\""
+msgstr "PL/Python -ის პროცედურა \"%s\""
+
+#: plpy_main.c:355
+#, c-format
+msgid "PL/Python function \"%s\""
+msgstr "PL/Python -ის ფუნქცია \"%s\""
+
+#: plpy_main.c:363
+#, c-format
+msgid "PL/Python anonymous code block"
+msgstr "PL/Python -ის ანონიმური კოდის ბლოკი"
+
+#: plpy_plpymodule.c:168 plpy_plpymodule.c:171
+#, c-format
+msgid "could not import \"plpy\" module"
+msgstr "\"plpy\" მოდულის შემოტანის შეცდომა"
+
+#: plpy_plpymodule.c:182
+#, c-format
+msgid "could not create the spiexceptions module"
+msgstr "spiexceptions მოდულის შექმნის შეცდომა"
+
+#: plpy_plpymodule.c:190
+#, c-format
+msgid "could not add the spiexceptions module"
+msgstr "spiexceptions მოდულის დამატების შეცდომა"
+
+#: plpy_plpymodule.c:257
+#, c-format
+msgid "could not generate SPI exceptions"
+msgstr "\"SPI\" გამონაკლისების გენერაციის შეცდომა"
+
+#: plpy_plpymodule.c:425
+#, c-format
+msgid "could not unpack arguments in plpy.elog"
+msgstr "plpy.elog-ში არგუმენტების გაშლის შეცდომა"
+
+#: plpy_plpymodule.c:434
+msgid "could not parse error message in plpy.elog"
+msgstr "plpy.elog-ში შეცდომის შეტყობინების დამუშავების შეცდომა"
+
+#: plpy_plpymodule.c:451
+#, c-format
+msgid "argument 'message' given by name and position"
+msgstr "არგუმენტი 'message', მოცემული სახელითა მდებარეობით"
+
+#: plpy_plpymodule.c:478
+#, c-format
+msgid "'%s' is an invalid keyword argument for this function"
+msgstr "%s ამ ფუნქციის არასწორი არგუმენტია"
+
+#: plpy_plpymodule.c:489 plpy_plpymodule.c:495
+#, c-format
+msgid "invalid SQLSTATE code"
+msgstr "არასწორი SQLSTATE კოდი"
+
+#: plpy_procedure.c:225
+#, c-format
+msgid "trigger functions can only be called as triggers"
+msgstr "ტრიგერის ფუნქციების გამოძახება მხოლოდ ტრიგერებად შეიძლება"
+
+#: plpy_procedure.c:229
+#, c-format
+msgid "PL/Python functions cannot return type %s"
+msgstr "PL/Python ფუნქციებს ამ ტიპის დაბრუნება არ შეუძლიათ: %s"
+
+#: plpy_procedure.c:307
+#, c-format
+msgid "PL/Python functions cannot accept type %s"
+msgstr "PL/Python ფუნქციებს ამ ტიპის მიღება არ შეუძლიათ: %s"
+
+#: plpy_procedure.c:395
+#, c-format
+msgid "could not compile PL/Python function \"%s\""
+msgstr "\"PL/Python\"-ის ფუნქციის კომპილაციის შეცდომა: \"%s\""
+
+#: plpy_procedure.c:398
+#, c-format
+msgid "could not compile anonymous PL/Python code block"
+msgstr "\"PL/Python\"-ის კოდის ანონიმური ბლოკის კომპილაციის შეცდომა"
+
+#: plpy_resultobject.c:117 plpy_resultobject.c:143 plpy_resultobject.c:169
+#, c-format
+msgid "command did not produce a result set"
+msgstr "ბრძანებამ შედეგი არ გამოიღო"
+
+#: plpy_spi.c:56
+#, c-format
+msgid "second argument of plpy.prepare must be a sequence"
+msgstr "plpy.prepare -ის მეორე არგუმენტი მიმდევრობა უნდა იყოს"
+
+#: plpy_spi.c:98
+#, c-format
+msgid "plpy.prepare: type name at ordinal position %d is not a string"
+msgstr "plpy.prepare: ტიპის სახელი ორდინალურ მდგომარეობაში %d სტრიქონს არ წარმოადგენს"
+
+#: plpy_spi.c:170
+#, c-format
+msgid "plpy.execute expected a query or a plan"
+msgstr "plpy.execute გეგმას ან მოთხოვნას მოელოდა"
+
+#: plpy_spi.c:189
+#, c-format
+msgid "plpy.execute takes a sequence as its second argument"
+msgstr "plpy.execute მეორე არგუმენტად მიმდევრობას იღებს"
+
+#: plpy_spi.c:297
+#, c-format
+msgid "SPI_execute_plan failed: %s"
+msgstr "SPI_execute_plan -ის შეცდომა: %s"
+
+#: plpy_spi.c:339
+#, c-format
+msgid "SPI_execute failed: %s"
+msgstr "SPI_execute -ის შეცდომა: %s"
+
+#: plpy_subxactobject.c:92
+#, c-format
+msgid "this subtransaction has already been entered"
+msgstr "ეს ქვეტრანზაქცია უკვე დაიწყო"
+
+#: plpy_subxactobject.c:98 plpy_subxactobject.c:156
+#, c-format
+msgid "this subtransaction has already been exited"
+msgstr "ეს ქვეტრანზაქცია უკვე დასრულდა"
+
+#: plpy_subxactobject.c:150
+#, c-format
+msgid "this subtransaction has not been entered"
+msgstr "ეს ქვეტრანზაქცია არ დაწყებულა"
+
+#: plpy_subxactobject.c:162
+#, c-format
+msgid "there is no subtransaction to exit from"
+msgstr "გამოსასვლელი ქვეტრანზაქციები არ არსებობს"
+
+#: plpy_typeio.c:588
+#, c-format
+msgid "could not import a module for Decimal constructor"
+msgstr "ათობითი კონსტრუქტორისთვის მოდულის შემოტანის პრობლემა"
+
+#: plpy_typeio.c:592
+#, c-format
+msgid "no Decimal attribute in module"
+msgstr "მოდულს ათობითს ატრიბუტი არ გააჩნია"
+
+#: plpy_typeio.c:598
+#, c-format
+msgid "conversion from numeric to Decimal failed"
+msgstr "რიცხვითიდან ათობითში გადაყვანის შეცდომა"
+
+#: plpy_typeio.c:912
+#, c-format
+msgid "could not create bytes representation of Python object"
+msgstr "\"Python\"-ის ობექტის ბაიტების რეპრეზენტაციის შექმნის შეცდომა"
+
+#: plpy_typeio.c:1049
+#, c-format
+msgid "could not create string representation of Python object"
+msgstr "\"Python\"-ის ობექტის სტრიქონების რეპრეზენტაციის შექმნის შეცდომა"
+
+#: plpy_typeio.c:1060
+#, c-format
+msgid "could not convert Python object into cstring: Python string representation appears to contain null bytes"
+msgstr "შეცდომა Python-ის ობიექტის cstring-ში გადაყვანისას: Python-ის სტრიქონის გამოხატულება ნულოვან ბაიტებს შეიცავს"
+
+#: plpy_typeio.c:1157
+#, c-format
+msgid "return value of function with array return type is not a Python sequence"
+msgstr "ფუნქციის დაბრულების მნიშვნელობა მასივის დაბრუნების ტიპით Python-ის მიმდევრობა არაა"
+
+#: plpy_typeio.c:1202
+#, c-format
+msgid "could not determine sequence length for function return value"
+msgstr "ფუნქციის დაბრუნებული მნიშვნელობის მიმდევრობის სიგრძის განსაზღვრა შეუძლებელია"
+
+#: plpy_typeio.c:1222 plpy_typeio.c:1237 plpy_typeio.c:1253
+#, c-format
+msgid "multidimensional arrays must have array expressions with matching dimensions"
+msgstr "მრავალგანზომილებიან მასივებს უნდა ჰქონდეთ მასივის გამოსახულებები შესაბამისი ზომებით"
+
+#: plpy_typeio.c:1227
+#, c-format
+msgid "number of array dimensions exceeds the maximum allowed (%d)"
+msgstr "მასივის ზომების რაოდენობა მაქსიმუმ დასაშვებზე (%d) დიდია"
+
+#: plpy_typeio.c:1329
+#, c-format
+msgid "malformed record literal: \"%s\""
+msgstr "ჩანაწერის არასწორი სტრიქონი: %s"
+
+#: plpy_typeio.c:1330
+#, c-format
+msgid "Missing left parenthesis."
+msgstr "აკლია მარჯვენა ფრჩხილი."
+
+#: plpy_typeio.c:1331 plpy_typeio.c:1532
+#, c-format
+msgid "To return a composite type in an array, return the composite type as a Python tuple, e.g., \"[('foo',)]\"."
+msgstr "მასივში კომპოზიტური ტიპის დასაბრუნებლად კომპოზიტური ტიპი, როგორც Python-ის კოლაჟი, ისე დააბრუნეთ. მაგ: \"[('foo',)]\"."
+
+#: plpy_typeio.c:1378
+#, c-format
+msgid "key \"%s\" not found in mapping"
+msgstr "ბმაში გასაღები %s ნაპოვნი არაა"
+
+#: plpy_typeio.c:1379
+#, c-format
+msgid "To return null in a column, add the value None to the mapping with the key named after the column."
+msgstr "სვეტში ნულის დასაბრუნებლად ამ სვეტის სახელის მქონე გასაღების მიბმას მნიშვნელობა None დაამატეთ."
+
+#: plpy_typeio.c:1432
+#, c-format
+msgid "length of returned sequence did not match number of columns in row"
+msgstr "დაბრუნებული მიმდევრობის სიგრძე მწკრივში სვეტების რაოდენობას არ ემთხვევა"
+
+#: plpy_typeio.c:1530
+#, c-format
+msgid "attribute \"%s\" does not exist in Python object"
+msgstr "ატრიბუტი \"%s\" Python -ის ობიექტში არ არსებობს"
+
+#: plpy_typeio.c:1533
+#, c-format
+msgid "To return null in a column, let the returned object have an attribute named after column with value None."
+msgstr "სვეტში ნულის დასაბრუნებლად დასაბრუნებელ ობიექტს მიანიჭეთ სვეტის სახელის მქონე ატრიბუტი , მნიშვნელობით \"None\"."
+
+#: plpy_util.c:31
+#, c-format
+msgid "could not convert Python Unicode object to bytes"
+msgstr "\"Python Unicode\" ტიპის ობიექტის ბაიტებად გარდაქმნის შეცდომა"
+
+#: plpy_util.c:37
+#, c-format
+msgid "could not extract bytes from encoded string"
+msgstr "ბაიტების ამოღების შეცდომა კოდირებული სტრიქონიდან"
+
+#, c-format
+#~ msgid "To construct a multidimensional array, the inner sequences must all have the same length."
+#~ msgstr "მრავალგანზომილებიანი მასივის ასაშენებლად ყველა შიდა მიმდევრობის სიგრძე ტოლი უნდა იყოს."
+
+#, c-format
+#~ msgid "array size exceeds the maximum allowed"
+#~ msgstr "მასივის ზომა მაქსიმალურ დასაშვებს აჭარბებს"
+
+#, c-format
+#~ msgid "wrong length of inner sequence: has length %d, but %d was expected"
+#~ msgstr "შიდა მიმდევრობის არასწორი სიგრძე: სიგრძე: %d. უნდა იყოს: %d"
diff --git a/src/pl/plpython/po/ko.po b/src/pl/plpython/po/ko.po
new file mode 100644
index 0000000..ef64d5c
--- /dev/null
+++ b/src/pl/plpython/po/ko.po
@@ -0,0 +1,478 @@
+# LANGUAGE message translation file for plpython
+# Copyright (C) 2015 PostgreSQL Global Development Group
+# This file is distributed under the same license as the PostgreSQL package.
+# Ioseph Kim <ioseph@uri.sarang.net>, 2015.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: plpython (PostgreSQL) 16\n"
+"Report-Msgid-Bugs-To: pgsql-bugs@lists.postgresql.org\n"
+"POT-Creation-Date: 2023-09-07 05:38+0000\n"
+"PO-Revision-Date: 2023-05-30 12:40+0900\n"
+"Last-Translator: Ioseph Kim <ioseph@uri.sarang.net>\n"
+"Language-Team: Korean <pgsql-kr@postgresql.kr>\n"
+"Language: ko\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=1; plural=0;\n"
+
+#: plpy_cursorobject.c:72
+#, c-format
+msgid "plpy.cursor expected a query or a plan"
+msgstr "plpy.cursor 객체는 쿼리나 plpy.prepare 객체를 인자로 사용합니다"
+
+#: plpy_cursorobject.c:155
+#, c-format
+msgid "plpy.cursor takes a sequence as its second argument"
+msgstr ""
+"plpy.cursor 객체의 인자로 plpy.prepare 객체를 사용한 경우 두번째 인자는 "
+"prepare 객체의 매개변수가 있어야 합니다."
+
+#: plpy_cursorobject.c:171 plpy_spi.c:205
+#, c-format
+msgid "could not execute plan"
+msgstr "plpy.prepare 객체를 실행할 수 없음"
+
+#: plpy_cursorobject.c:174 plpy_spi.c:208
+#, c-format
+msgid "Expected sequence of %d argument, got %d: %s"
+msgid_plural "Expected sequence of %d arguments, got %d: %s"
+msgstr[0] "%d 개의 인자가 필요한데, %d개의 인자를 지정했음: %s"
+
+#: plpy_cursorobject.c:321
+#, c-format
+msgid "iterating a closed cursor"
+msgstr "이미 닫긴 커서에서 다음 자료를 요구하고 있음"
+
+#: plpy_cursorobject.c:329 plpy_cursorobject.c:395
+#, c-format
+msgid "iterating a cursor in an aborted subtransaction"
+msgstr "중지된 서브 트랜잭션에 있는 커서에서 다음 자료를 요구하고 있음"
+
+#: plpy_cursorobject.c:387
+#, c-format
+msgid "fetch from a closed cursor"
+msgstr "닫긴 커서에서 fetch"
+
+#: plpy_cursorobject.c:430 plpy_spi.c:401
+#, c-format
+msgid "query result has too many rows to fit in a Python list"
+msgstr "쿼리 결과가 Python 리스트로 담기에는 너무 많습니다"
+
+#: plpy_cursorobject.c:482
+#, c-format
+msgid "closing a cursor in an aborted subtransaction"
+msgstr "중지된 서브트랜잭션에서 커서를 닫고 있음"
+
+#: plpy_elog.c:125 plpy_elog.c:126 plpy_plpymodule.c:530
+#, c-format
+msgid "%s"
+msgstr "%s"
+
+#: plpy_exec.c:139
+#, c-format
+msgid "unsupported set function return mode"
+msgstr "지원하지 않는 집합 함수 리턴 모드"
+
+#: plpy_exec.c:140
+#, c-format
+msgid ""
+"PL/Python set-returning functions only support returning one value per call."
+msgstr ""
+"PL/Python 집합-반환 함수는 한번의 호출에 대해서 하나의 값만 반환할 수 있습니"
+"다."
+
+#: plpy_exec.c:153
+#, c-format
+msgid "returned object cannot be iterated"
+msgstr "반환하는 객체가 iterable 형이 아님"
+
+#: plpy_exec.c:154
+#, c-format
+msgid "PL/Python set-returning functions must return an iterable object."
+msgstr "PL/Python 집합-반환 함수는 iterable 객체를 반환해야 합니다."
+
+#: plpy_exec.c:168
+#, c-format
+msgid "error fetching next item from iterator"
+msgstr "iterator에서 다음 아이템을 가져올 수 없음"
+
+#: plpy_exec.c:211
+#, c-format
+msgid "PL/Python procedure did not return None"
+msgstr "PL/Python 프로시져가 None을 반환하지 않았음"
+
+#: plpy_exec.c:215
+#, c-format
+msgid "PL/Python function with return type \"void\" did not return None"
+msgstr ""
+"반환 자료형이 \"void\"인 PL/Python 함수가 return None으로 끝나지 않았음"
+
+#: plpy_exec.c:369 plpy_exec.c:393
+#, c-format
+msgid "unexpected return value from trigger procedure"
+msgstr "트리거 프로시져가 예상치 못한 값을 반환했습니다"
+
+#: plpy_exec.c:370
+#, c-format
+msgid "Expected None or a string."
+msgstr "None 이나 문자열이 있어야합니다."
+
+#: plpy_exec.c:383
+#, c-format
+msgid ""
+"PL/Python trigger function returned \"MODIFY\" in a DELETE trigger -- ignored"
+msgstr ""
+"PL/Python 트리거 함수가 DELETE 트리거에서 \"MODIFY\"를 반환했음 -- 무시함"
+
+#: plpy_exec.c:394
+#, c-format
+msgid "Expected None, \"OK\", \"SKIP\", or \"MODIFY\"."
+msgstr "None, \"OK\", \"SKIP\", 또는 \"MODIFY\"를 사용해야 함."
+
+#: plpy_exec.c:444
+#, c-format
+msgid "PyList_SetItem() failed, while setting up arguments"
+msgstr "PyList_SetItem() 함수가 인자 설정하는 중 실패"
+
+#: plpy_exec.c:448
+#, c-format
+msgid "PyDict_SetItemString() failed, while setting up arguments"
+msgstr "PyDict_SetItemString() 함수가 인자 설정하는 중 실패"
+
+#: plpy_exec.c:460
+#, c-format
+msgid ""
+"function returning record called in context that cannot accept type record"
+msgstr "반환 자료형이 record인데 함수가 그 자료형으로 반환하지 않음"
+
+#: plpy_exec.c:677
+#, c-format
+msgid "while creating return value"
+msgstr "반환값을 만들고 있은 중"
+
+#: plpy_exec.c:924
+#, c-format
+msgid "TD[\"new\"] deleted, cannot modify row"
+msgstr "TD[\"new\"] 변수가 삭제되었음, 로우를 수정할 수 없음"
+
+#: plpy_exec.c:929
+#, c-format
+msgid "TD[\"new\"] is not a dictionary"
+msgstr "TD[\"new\"] 변수가 딕션너리 형태가 아님"
+
+#: plpy_exec.c:954
+#, c-format
+msgid "TD[\"new\"] dictionary key at ordinal position %d is not a string"
+msgstr "%d 번째 TD[\"new\"] 딕션너리 키가 문자열이 아님"
+
+#: plpy_exec.c:961
+#, c-format
+msgid ""
+"key \"%s\" found in TD[\"new\"] does not exist as a column in the triggering "
+"row"
+msgstr ""
+"로우 트리거 작업에서 칼럼으로 사용되는 \"%s\" 키가 TD[\"new\"] 변수에 없음."
+
+#: plpy_exec.c:966
+#, c-format
+msgid "cannot set system attribute \"%s\""
+msgstr "\"%s\" 시스템 속성을 지정할 수 없음"
+
+#: plpy_exec.c:971
+#, c-format
+msgid "cannot set generated column \"%s\""
+msgstr "\"%s\" 계산된 칼럼을 지정할 수 없음"
+
+#: plpy_exec.c:1029
+#, c-format
+msgid "while modifying trigger row"
+msgstr "로우 변경 트리거 작업 도중"
+
+#: plpy_exec.c:1087
+#, c-format
+msgid "forcibly aborting a subtransaction that has not been exited"
+msgstr "서브트랜잭션이 중지됨으로 강제로 중지됨"
+
+#: plpy_main.c:109
+#, c-format
+msgid "multiple Python libraries are present in session"
+msgstr "세션에서 여러 Python 라이브러리가 사용되고 있습니다"
+
+#: plpy_main.c:110
+#, c-format
+msgid "Only one Python major version can be used in one session."
+msgstr "하나의 세션에서는 하나의 Python 메이져 버전만 사용할 수 있습니다."
+
+#: plpy_main.c:122
+#, c-format
+msgid "untrapped error in initialization"
+msgstr "plpy 모듈 초기화 실패"
+
+#: plpy_main.c:145
+#, c-format
+msgid "could not import \"__main__\" module"
+msgstr "\"__main__\" 모듈은 임포트 할 수 없음"
+
+#: plpy_main.c:154
+#, c-format
+msgid "could not initialize globals"
+msgstr "전역변수들을 초기화 할 수 없음"
+
+#: plpy_main.c:352
+#, c-format
+msgid "PL/Python procedure \"%s\""
+msgstr "\"%s\" PL/Python 프로시져"
+
+#: plpy_main.c:355
+#, c-format
+msgid "PL/Python function \"%s\""
+msgstr "\"%s\" PL/Python 함수"
+
+#: plpy_main.c:363
+#, c-format
+msgid "PL/Python anonymous code block"
+msgstr "PL/Python 익명 코드 블럭"
+
+#: plpy_plpymodule.c:168 plpy_plpymodule.c:171
+#, c-format
+msgid "could not import \"plpy\" module"
+msgstr "\"plpy\" 모듈을 임포트 할 수 없음"
+
+#: plpy_plpymodule.c:182
+#, c-format
+msgid "could not create the spiexceptions module"
+msgstr "spiexceptions 모듈을 만들 수 없음"
+
+#: plpy_plpymodule.c:190
+#, c-format
+msgid "could not add the spiexceptions module"
+msgstr "spiexceptions 모듈을 추가할 수 없음"
+
+#: plpy_plpymodule.c:257
+#, c-format
+msgid "could not generate SPI exceptions"
+msgstr "SPI 예외처리를 생성할 수 없음"
+
+#: plpy_plpymodule.c:425
+#, c-format
+msgid "could not unpack arguments in plpy.elog"
+msgstr "잘못된 인자로 구성된 plpy.elog"
+
+#: plpy_plpymodule.c:434
+msgid "could not parse error message in plpy.elog"
+msgstr "plpy.elog 에서 오류 메시지를 분석할 수 없음"
+
+#: plpy_plpymodule.c:451
+#, c-format
+msgid "argument 'message' given by name and position"
+msgstr "'message' 인자는 이름과 위치가 있어야 함"
+
+#: plpy_plpymodule.c:478
+#, c-format
+msgid "'%s' is an invalid keyword argument for this function"
+msgstr "'%s' 값은 이 함수에서 잘못된 예약어 인자입니다"
+
+#: plpy_plpymodule.c:489 plpy_plpymodule.c:495
+#, c-format
+msgid "invalid SQLSTATE code"
+msgstr "잘못된 SQLSTATE 코드"
+
+#: plpy_procedure.c:225
+#, c-format
+msgid "trigger functions can only be called as triggers"
+msgstr "트리거 함수는 트리거로만 호출될 수 있음"
+
+#: plpy_procedure.c:229
+#, c-format
+msgid "PL/Python functions cannot return type %s"
+msgstr "PL/Python 함수는 %s 자료형을 반환할 수 없음"
+
+#: plpy_procedure.c:307
+#, c-format
+msgid "PL/Python functions cannot accept type %s"
+msgstr "PL/Python 함수는 %s 자료형을 사용할 수 없음"
+
+#: plpy_procedure.c:395
+#, c-format
+msgid "could not compile PL/Python function \"%s\""
+msgstr "\"%s\" PL/Python 함수를 컴파일 할 수 없음"
+
+#: plpy_procedure.c:398
+#, c-format
+msgid "could not compile anonymous PL/Python code block"
+msgstr "anonymous PL/Python 코드 블록을 컴파일 할 수 없음"
+
+#: plpy_resultobject.c:117 plpy_resultobject.c:143 plpy_resultobject.c:169
+#, c-format
+msgid "command did not produce a result set"
+msgstr "명령의 결과값이 없음"
+
+#: plpy_spi.c:56
+#, c-format
+msgid "second argument of plpy.prepare must be a sequence"
+msgstr "plpy.prepare 함수의 두번째 인자는 Python 시퀀스형이어야 함"
+
+#: plpy_spi.c:98
+#, c-format
+msgid "plpy.prepare: type name at ordinal position %d is not a string"
+msgstr "plpy.prepare: %d 번째 인자의 자료형이 문자열이 아님"
+
+#: plpy_spi.c:170
+#, c-format
+msgid "plpy.execute expected a query or a plan"
+msgstr "plpy.execute 함수의 인자는 쿼리문이나 plpy.prepare 객체여야 함"
+
+#: plpy_spi.c:189
+#, c-format
+msgid "plpy.execute takes a sequence as its second argument"
+msgstr "plpy.execut 함수의 두번째 인자는 python 시퀀스형이 와야함"
+
+#: plpy_spi.c:297
+#, c-format
+msgid "SPI_execute_plan failed: %s"
+msgstr "SPI_execute_plan 실패: %s"
+
+#: plpy_spi.c:339
+#, c-format
+msgid "SPI_execute failed: %s"
+msgstr "SPI_execute 실패: %s"
+
+#: plpy_subxactobject.c:92
+#, c-format
+msgid "this subtransaction has already been entered"
+msgstr "이 서브트랜잭션은 이미 시작되었음"
+
+#: plpy_subxactobject.c:98 plpy_subxactobject.c:156
+#, c-format
+msgid "this subtransaction has already been exited"
+msgstr "이 서브트랜잭션은 이미 끝났음"
+
+#: plpy_subxactobject.c:150
+#, c-format
+msgid "this subtransaction has not been entered"
+msgstr "이 서브트랜잭션이 시작되지 않았음"
+
+#: plpy_subxactobject.c:162
+#, c-format
+msgid "there is no subtransaction to exit from"
+msgstr "종료할 서브트랜잭션이 없음, 위치:"
+
+#: plpy_typeio.c:588
+#, c-format
+msgid "could not import a module for Decimal constructor"
+msgstr "Decimal 자료형 처리를 위해 모듈을 임포트 할 수 없음"
+
+#: plpy_typeio.c:592
+#, c-format
+msgid "no Decimal attribute in module"
+msgstr "모듈안에 Decimal 속성이 없음"
+
+#: plpy_typeio.c:598
+#, c-format
+msgid "conversion from numeric to Decimal failed"
+msgstr "numeric 형을 Decimal 형으로 변환할 수 없음"
+
+#: plpy_typeio.c:912
+#, c-format
+msgid "could not create bytes representation of Python object"
+msgstr "Python 객체를 bytea 자료형으로 변환할 수 없음"
+
+#: plpy_typeio.c:1049
+#, c-format
+msgid "could not create string representation of Python object"
+msgstr "Python 객체를 문자열 자료형으로 변환할 수 없음"
+
+#: plpy_typeio.c:1060
+#, c-format
+msgid ""
+"could not convert Python object into cstring: Python string representation "
+"appears to contain null bytes"
+msgstr ""
+"Python 객체를 cstring 형으로 변환할 수 없음: Python string 변수에 null문자열"
+"이 포함되어 있음"
+
+#: plpy_typeio.c:1157
+#, c-format
+msgid ""
+"return value of function with array return type is not a Python sequence"
+msgstr "배열형으로 넘길 자료형이 Python 시퀀스형이 아님"
+
+#: plpy_typeio.c:1202
+#, c-format
+msgid "could not determine sequence length for function return value"
+msgstr "함수 반환 값으로 시퀀스 길이를 결정할 수 없음"
+
+#: plpy_typeio.c:1222 plpy_typeio.c:1237 plpy_typeio.c:1253
+#, c-format
+msgid ""
+"multidimensional arrays must have array expressions with matching dimensions"
+msgstr "다차원 배열에는 일치하는 차원이 포함된 배열 식이 있어야 함"
+
+#: plpy_typeio.c:1227
+#, c-format
+msgid "number of array dimensions exceeds the maximum allowed (%d)"
+msgstr "배열 차원이 최대치 (%d)를 초과 했습니다."
+
+#: plpy_typeio.c:1329
+#, c-format
+msgid "malformed record literal: \"%s\""
+msgstr "잘못된 레코드 표현: \"%s\""
+
+#: plpy_typeio.c:1330
+#, c-format
+msgid "Missing left parenthesis."
+msgstr "왼쪽 괄호가 없음."
+
+#: plpy_typeio.c:1331 plpy_typeio.c:1532
+#, c-format
+msgid ""
+"To return a composite type in an array, return the composite type as a "
+"Python tuple, e.g., \"[('foo',)]\"."
+msgstr ""
+"배열에서 복합 자료형을 반환하려면, Python 튜플 형을 사용하세요. 예: "
+"\"[('foo',)]\"."
+
+#: plpy_typeio.c:1378
+#, c-format
+msgid "key \"%s\" not found in mapping"
+msgstr "맵 안에 \"%s\" 키가 없음"
+
+#: plpy_typeio.c:1379
+#, c-format
+msgid ""
+"To return null in a column, add the value None to the mapping with the key "
+"named after the column."
+msgstr ""
+"칼럼값으로 null을 반환하려면, 칼럼 다음에 해당 키 이름과 맵핑 되는 None값을 "
+"지정하세요"
+
+#: plpy_typeio.c:1432
+#, c-format
+msgid "length of returned sequence did not match number of columns in row"
+msgstr "반환되는 시퀀스형 변수의 길이가 로우의 칼럼수와 일치하지 않음"
+
+#: plpy_typeio.c:1530
+#, c-format
+msgid "attribute \"%s\" does not exist in Python object"
+msgstr "Python 객체 가운데 \"%s\" 속성이 없음"
+
+#: plpy_typeio.c:1533
+#, c-format
+msgid ""
+"To return null in a column, let the returned object have an attribute named "
+"after column with value None."
+msgstr ""
+"칼럼 값으로 null 을 반환하려면, 값으로 None 값을 가지는 칼럼 뒤에, 속성 이름"
+"이 있는 객체를 반환하세요"
+
+#: plpy_util.c:31
+#, c-format
+msgid "could not convert Python Unicode object to bytes"
+msgstr "Python 유니코드 객체를 UTF-8 문자열로 변환할 수 없음"
+
+#: plpy_util.c:37
+#, c-format
+msgid "could not extract bytes from encoded string"
+msgstr "해당 인코드 문자열을 Python에서 사용할 수 없음"
diff --git a/src/pl/plpython/po/meson.build b/src/pl/plpython/po/meson.build
new file mode 100644
index 0000000..ddaa191
--- /dev/null
+++ b/src/pl/plpython/po/meson.build
@@ -0,0 +1,3 @@
+# Copyright (c) 2022-2023, PostgreSQL Global Development Group
+
+nls_targets += [i18n.gettext('plpython-' + pg_version_major.to_string())]
diff --git a/src/pl/plpython/po/pl.po b/src/pl/plpython/po/pl.po
new file mode 100644
index 0000000..4abd6f5
--- /dev/null
+++ b/src/pl/plpython/po/pl.po
@@ -0,0 +1,507 @@
+# plpython message translation file for plpython
+# Copyright (C) 2011 PostgreSQL Global Development Group
+# This file is distributed under the same license as the PostgreSQL package.
+# Begina Felicysym <begina.felicysym@wp.eu>, 2011, 2012.
+# grzegorz <begina.felicysym@wp.eu>, 2014, 2015, 2016, 2017.
+msgid ""
+msgstr ""
+"Project-Id-Version: plpython (PostgreSQL 9.1)\n"
+"Report-Msgid-Bugs-To: pgsql-bugs@postgresql.org\n"
+"POT-Creation-Date: 2017-04-09 21:07+0000\n"
+"PO-Revision-Date: 2017-04-11 23:15+0200\n"
+"Last-Translator: grzegorz <begina.felicysym@wp.eu>\n"
+"Language-Team: begina.felicysym@wp.eu\n"
+"Language: pl\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 "
+"|| n%100>=20) ? 1 : 2);\n"
+"X-Generator: Virtaal 0.7.1\n"
+
+#: plpy_cursorobject.c:100
+#, c-format
+msgid "plpy.cursor expected a query or a plan"
+msgstr "plpy.cursor oczekuje kwerendy lub planu"
+
+#: plpy_cursorobject.c:176
+#, c-format
+msgid "plpy.cursor takes a sequence as its second argument"
+msgstr "plpy.cursor przyjmuje sekwencję jako drugi argument"
+
+#: plpy_cursorobject.c:192 plpy_spi.c:226
+#, c-format
+msgid "could not execute plan"
+msgstr "nie można wykonać planu"
+
+#: plpy_cursorobject.c:195 plpy_spi.c:229
+#, c-format
+msgid "Expected sequence of %d argument, got %d: %s"
+msgid_plural "Expected sequence of %d arguments, got %d: %s"
+msgstr[0] "Oczekiwano sekwencji z %d argumentem, mamy %d: %s"
+msgstr[1] "Oczekiwano sekwencji z %d argumentami, mamy %d: %s"
+msgstr[2] "Oczekiwano sekwencji z %d argumentami, mamy %d: %s"
+
+#: plpy_cursorobject.c:350
+#, c-format
+msgid "iterating a closed cursor"
+msgstr "iteracja zamkniętego kursora"
+
+#: plpy_cursorobject.c:358 plpy_cursorobject.c:423
+#, c-format
+msgid "iterating a cursor in an aborted subtransaction"
+msgstr "iteracja kursora w przerwanej podtransakcji"
+
+#: plpy_cursorobject.c:415
+#, c-format
+msgid "fetch from a closed cursor"
+msgstr "pobranie z zamkniętego kursora"
+
+#: plpy_cursorobject.c:463 plpy_spi.c:434
+#, c-format
+msgid "query result has too many rows to fit in a Python list"
+msgstr "wynik zapytania ma za dużo wierszy by pomieścić w liście Python"
+
+#: plpy_cursorobject.c:504
+#, c-format
+msgid "closing a cursor in an aborted subtransaction"
+msgstr "zamknięcie kursora w przerwanej podtransakcji"
+
+#: plpy_elog.c:127 plpy_elog.c:128 plpy_plpymodule.c:548
+#, c-format
+msgid "%s"
+msgstr "%s"
+
+#: plpy_exec.c:140
+#, c-format
+msgid "unsupported set function return mode"
+msgstr "nieobsługiwany tryb zwracania przez funkcję grupy"
+
+#: plpy_exec.c:141
+#, c-format
+msgid "PL/Python set-returning functions only support returning one value per call."
+msgstr "funkcje zwracające grupę PL/Python obsługuje tylko zwracanie jednej wartości w wywołaniu."
+
+#: plpy_exec.c:154
+#, c-format
+msgid "returned object cannot be iterated"
+msgstr "zwrócony obiekt nie może być przeiterowany"
+
+#: plpy_exec.c:155
+#, c-format
+msgid "PL/Python set-returning functions must return an iterable object."
+msgstr "funkcje PL/Python zwracające grupę muszą zwracać iterowalny obiekt."
+
+#: plpy_exec.c:169
+#, c-format
+msgid "error fetching next item from iterator"
+msgstr "błąd pobierania następnego elementu z iteratora"
+
+#: plpy_exec.c:210
+#, c-format
+msgid "PL/Python function with return type \"void\" did not return None"
+msgstr "funkcja PL/Python zwracająca typ \"void\" nie zwróciła wartości None"
+
+#: plpy_exec.c:379 plpy_exec.c:405
+#, c-format
+msgid "unexpected return value from trigger procedure"
+msgstr "nieoczekiwana wartość zwracana przez procedury wyzwalacza"
+
+#: plpy_exec.c:380
+#, c-format
+msgid "Expected None or a string."
+msgstr "Oczekiwano None lub ciąg znaków."
+
+#: plpy_exec.c:395
+#, c-format
+msgid "PL/Python trigger function returned \"MODIFY\" in a DELETE trigger -- ignored"
+msgstr "funkcja wyzwalacza PL/Python zwróciła \"MODIFY\" w wyzwalaczu DELETE -- zignorowano"
+
+#: plpy_exec.c:406
+#, c-format
+msgid "Expected None, \"OK\", \"SKIP\", or \"MODIFY\"."
+msgstr "Oczekiwano None, \"OK\", \"SKIP\", lub \"MODIFY\"."
+
+#: plpy_exec.c:487
+#, c-format
+msgid "PyList_SetItem() failed, while setting up arguments"
+msgstr "nie powiodło się PyList_SetItem() podczas ustawiania argumentów"
+
+#: plpy_exec.c:491
+#, c-format
+msgid "PyDict_SetItemString() failed, while setting up arguments"
+msgstr "nie powiodło się PyDict_SetItemString() podczas ustawiania argumentów"
+
+#: plpy_exec.c:503
+#, c-format
+msgid "function returning record called in context that cannot accept type record"
+msgstr "funkcja zwracająca rekord w wywołaniu, które nie akceptuje typów złożonych"
+
+#: plpy_exec.c:719
+#, c-format
+msgid "while creating return value"
+msgstr "podczas tworzenia wartości zwracanej"
+
+#: plpy_exec.c:743
+#, c-format
+msgid "could not create new dictionary while building trigger arguments"
+msgstr "nie można utworzyć nowego słownika w czasie tworzenia argumentów wyzwalacza"
+
+#: plpy_exec.c:931
+#, c-format
+msgid "TD[\"new\"] deleted, cannot modify row"
+msgstr "usunięto TD[\"new\"], nie można zmienić wiersza"
+
+#: plpy_exec.c:936
+#, c-format
+msgid "TD[\"new\"] is not a dictionary"
+msgstr "TD[\"new\"] nie jest słownikiem"
+
+#: plpy_exec.c:963
+#, c-format
+msgid "TD[\"new\"] dictionary key at ordinal position %d is not a string"
+msgstr "klucz słownika TD[\"new\"] na pozycji porządkowej %d nie jest ciągiem znaków"
+
+#: plpy_exec.c:970
+#, c-format
+msgid "key \"%s\" found in TD[\"new\"] does not exist as a column in the triggering row"
+msgstr "klucz \"%s\" znaleziony w TD[\"new\"] nie istnieje jako kolumna w wierszu obsługiwanym przez wyzwalacz"
+
+#: plpy_exec.c:975
+#, c-format
+msgid "cannot set system attribute \"%s\""
+msgstr "nie można ustawić atrybutu systemowego \"%s\""
+
+#: plpy_exec.c:1046
+#, c-format
+msgid "while modifying trigger row"
+msgstr "podczas modyfikowania wiersza wyzwalacza"
+
+#: plpy_exec.c:1107
+#, c-format
+msgid "forcibly aborting a subtransaction that has not been exited"
+msgstr "wymuszone przerywanie podtransakcji, która nie została zakończona"
+
+#: plpy_main.c:125
+#, c-format
+msgid "multiple Python libraries are present in session"
+msgstr "kilka bibliotek Python jest dostępne w sesji"
+
+#: plpy_main.c:126
+#, c-format
+msgid "Only one Python major version can be used in one session."
+msgstr "Tylko jedna podstawowa wersja Python może być używana w sesji."
+
+#: plpy_main.c:142
+#, c-format
+msgid "untrapped error in initialization"
+msgstr "niewyłapany błąd w inicjacji"
+
+#: plpy_main.c:165
+#, c-format
+msgid "could not import \"__main__\" module"
+msgstr "nie można zaimportować modułu \"__main__\""
+
+#: plpy_main.c:170
+#, c-format
+msgid "could not create globals"
+msgstr "nie można utworzyć zmiennych globalnych"
+
+#: plpy_main.c:174
+#, c-format
+msgid "could not initialize globals"
+msgstr "nie można zainicjować zmiennych globalnych"
+
+#: plpy_main.c:387
+#, c-format
+msgid "PL/Python function \"%s\""
+msgstr "funkcja PL/Python \"%s\""
+
+#: plpy_main.c:394
+#, c-format
+msgid "PL/Python anonymous code block"
+msgstr "anonimowy blok kodu PL/Python"
+
+#: plpy_plpymodule.c:181 plpy_plpymodule.c:184
+#, c-format
+msgid "could not import \"plpy\" module"
+msgstr "nie można zaimportować modułu \"plpy\""
+
+#: plpy_plpymodule.c:199
+#, c-format
+#| msgid "could not add the spiexceptions module"
+msgid "could not create the spiexceptions module"
+msgstr "nie udało się utworzyć modułu spiexceptions"
+
+#: plpy_plpymodule.c:207
+#, c-format
+msgid "could not add the spiexceptions module"
+msgstr "nie udało się dodać modułu spiexceptions"
+
+#: plpy_plpymodule.c:236
+#, c-format
+#| msgid "could not create directory \"%s\": %m"
+msgid "could not create exception \"%s\""
+msgstr "nie można utworzyć wyjątku \"%s\""
+
+#: plpy_plpymodule.c:271 plpy_plpymodule.c:275
+#, c-format
+msgid "could not generate SPI exceptions"
+msgstr "nie można wygenerować wyjątków SPI"
+
+#: plpy_plpymodule.c:443
+#, c-format
+msgid "could not unpack arguments in plpy.elog"
+msgstr "nie można rozpakować argumentów w plpy.elog"
+
+#: plpy_plpymodule.c:452
+msgid "could not parse error message in plpy.elog"
+msgstr "nie można przetworzyć komunikatu błędu w plpy.elog"
+
+#: plpy_plpymodule.c:469
+#, c-format
+msgid "Argument 'message' given by name and position"
+msgstr "Argument 'message' przekazany przez nazwę i pozycję"
+
+#: plpy_plpymodule.c:496
+#, c-format
+msgid "'%s' is an invalid keyword argument for this function"
+msgstr "'%s' jest niepoprawnym słowem kluczowym argumentu dla tej funkcji"
+
+#: plpy_plpymodule.c:507 plpy_plpymodule.c:513
+#, c-format
+msgid "invalid SQLSTATE code"
+msgstr "błędny kod SQLSTATE"
+
+#: plpy_procedure.c:230
+#, c-format
+msgid "trigger functions can only be called as triggers"
+msgstr "procedury wyzwalaczy mogą być wywoływane jedynie przez wyzwalacze"
+
+#: plpy_procedure.c:235
+#, c-format
+msgid "PL/Python functions cannot return type %s"
+msgstr "funkcje PL/Python nie mogą zwracać wartości typu %s"
+
+#: plpy_procedure.c:316
+#, c-format
+msgid "PL/Python functions cannot accept type %s"
+msgstr "funkcje PL/Python nie obsługują typu %s"
+
+#: plpy_procedure.c:412
+#, c-format
+msgid "could not compile PL/Python function \"%s\""
+msgstr "nie powiodła się kompilacja funkcji PL/Python \"%s\""
+
+#: plpy_procedure.c:415
+#, c-format
+msgid "could not compile anonymous PL/Python code block"
+msgstr "nie udało się skompilować anonimowego bloku kodu PL/Python"
+
+#: plpy_resultobject.c:145 plpy_resultobject.c:165 plpy_resultobject.c:185
+#, c-format
+msgid "command did not produce a result set"
+msgstr "polecenie nie utworzyło zbioru wynikowego"
+
+#: plpy_spi.c:59
+#, c-format
+msgid "second argument of plpy.prepare must be a sequence"
+msgstr "drugi argument plpy.prepare musi być sekwencją"
+
+#: plpy_spi.c:115
+#, c-format
+msgid "plpy.prepare: type name at ordinal position %d is not a string"
+msgstr "plpy.prepare: nazwa typu na pozycji porządkowej %d nie jest ciągiem znaków"
+
+#: plpy_spi.c:191
+#, c-format
+msgid "plpy.execute expected a query or a plan"
+msgstr "plpy.execute oczekuje kwerendy lub planu"
+
+#: plpy_spi.c:210
+#, c-format
+msgid "plpy.execute takes a sequence as its second argument"
+msgstr "plpy.execute przyjmuje sekwencję jako drugi argument"
+
+#: plpy_spi.c:335
+#, c-format
+msgid "SPI_execute_plan failed: %s"
+msgstr "nie powiódł się SPI_execute_plan: %s"
+
+#: plpy_spi.c:377
+#, c-format
+msgid "SPI_execute failed: %s"
+msgstr "nie powiódł się SPI_execute: %s"
+
+#: plpy_subxactobject.c:122
+#, c-format
+msgid "this subtransaction has already been entered"
+msgstr "ta podtransakcja już została wprowadzona"
+
+#: plpy_subxactobject.c:128 plpy_subxactobject.c:186
+#, c-format
+msgid "this subtransaction has already been exited"
+msgstr "ta podtransakcja już została zakończona"
+
+#: plpy_subxactobject.c:180
+#, c-format
+msgid "this subtransaction has not been entered"
+msgstr "ta podtransakcja nie została wprowadzona"
+
+#: plpy_subxactobject.c:192
+#, c-format
+msgid "there is no subtransaction to exit from"
+msgstr "brak podtransakcji by z niej wyjść"
+
+#: plpy_typeio.c:292
+#, c-format
+msgid "could not create new dictionary"
+msgstr "nie można utworzyć nowego słownika"
+
+#: plpy_typeio.c:560
+#, c-format
+msgid "could not import a module for Decimal constructor"
+msgstr "nie można zaimportować modułu dla konstruktora Decimal"
+
+#: plpy_typeio.c:564
+#, c-format
+msgid "no Decimal attribute in module"
+msgstr "brak atrybutu Decimal w module"
+
+#: plpy_typeio.c:570
+#, c-format
+msgid "conversion from numeric to Decimal failed"
+msgstr "konwersja z numeric na Decimal nie powiodła się"
+
+#: plpy_typeio.c:772
+#, c-format
+msgid "could not create bytes representation of Python object"
+msgstr "nie można utworzyć reprezentacji bajtowej obiektu Python"
+
+#: plpy_typeio.c:881
+#, c-format
+msgid "could not create string representation of Python object"
+msgstr "nie można utworzyć reprezentacji znakowej obiektu Python"
+
+#: plpy_typeio.c:892
+#, c-format
+msgid "could not convert Python object into cstring: Python string representation appears to contain null bytes"
+msgstr "nie można zmienić obiektu Python na cstring: reprezentacja ciągu znaków Python wydaje się zawierać puste bajty"
+
+#: plpy_typeio.c:949
+#, c-format
+msgid "malformed record literal: \"%s\""
+msgstr "nieprawidłowy literał rekordu: \"%s\""
+
+#: plpy_typeio.c:950
+#, c-format
+msgid "Missing left parenthesis."
+msgstr "Brak lewego nawiasu."
+
+#: plpy_typeio.c:951 plpy_typeio.c:1389
+#, c-format
+msgid "To return a composite type in an array, return the composite type as a Python tuple, e.g. \"[('foo')]\""
+msgstr ""
+"By zwrócić typ złożony w tablicy, zwracaj typ złożony jako krotkę Pythona, "
+"np. \"[('foo')]\""
+
+#: plpy_typeio.c:1000
+#, c-format
+#| msgid "number of array dimensions (%d) exceeds the maximum allowed (%d)"
+msgid "number of array dimensions exceeds the maximum allowed (%d)"
+msgstr "liczba wymiarów tablicy przekracza dozwolone maksimum (%d)"
+
+#: plpy_typeio.c:1004
+#, c-format
+#| msgid "cannot determine OID of function lo_truncate\n"
+msgid "cannot determine sequence length for function return value"
+msgstr ""
+"nie można ustalić długości sekwencji dla zwracanej wartości przez funkcję"
+
+#: plpy_typeio.c:1007 plpy_typeio.c:1011
+#, c-format
+#| msgid "array size exceeds the maximum allowed (%d)"
+msgid "array size exceeds the maximum allowed"
+msgstr "rozmiar tablicy przekracza dozwolone maksimum"
+
+#: plpy_typeio.c:1037
+#, c-format
+msgid "return value of function with array return type is not a Python sequence"
+msgstr "wartość zwrócona przez funkcję zwracającą tablicę nie jest sekwencją Python"
+
+#: plpy_typeio.c:1090
+#, c-format
+#| msgid "multidimensional arrays must have array expressions with matching dimensions"
+msgid "multidimensional arrays must have array expressions with matching dimensions. PL/Python function return value has sequence length %d while expected %d"
+msgstr ""
+"wielowymiarowe tablice muszą mieć wyrażenia tablicowe z pasującymi "
+"wymiarami. Wartość zwracana przez funkcję PL/Python ma długość sekwencji %d "
+"gdy oczekiwano %d"
+
+#: plpy_typeio.c:1212
+#, c-format
+msgid "key \"%s\" not found in mapping"
+msgstr "nie odnaleziono klucza \"%s\" w mapowaniu"
+
+#: plpy_typeio.c:1213
+#, c-format
+msgid "To return null in a column, add the value None to the mapping with the key named after the column."
+msgstr "Aby zwrócić null w kolumnie, dodaj wartość None do mapowania z kluczem nazwanym wedle kolumny."
+
+#: plpy_typeio.c:1264
+#, c-format
+msgid "length of returned sequence did not match number of columns in row"
+msgstr "długość zwróconej sekwencji nie jest równa liczbie kolumn w wierszu"
+
+#: plpy_typeio.c:1387
+#, c-format
+msgid "attribute \"%s\" does not exist in Python object"
+msgstr "atrybut \"%s\" nie istnieje w obiekcie Python"
+
+#: plpy_typeio.c:1390
+#, c-format
+msgid "To return null in a column, let the returned object have an attribute named after column with value None."
+msgstr "Aby zwrócić null w kolumnie, niech zwrócony obiekt posiada atrybut nazwany wedle kolumny z wartością None."
+
+#: plpy_util.c:36
+#, c-format
+msgid "could not convert Python Unicode object to bytes"
+msgstr "nie można zmienić obiektu unikodowego Python na bajty"
+
+#: plpy_util.c:42
+#, c-format
+msgid "could not extract bytes from encoded string"
+msgstr "nie można wyciągnąć bajtów z kodowanego ciągu znaków"
+
+#~ msgid "Python major version mismatch in session"
+#~ msgstr "niezgodna wersja główna Python w sesji"
+
+#~ msgid "This session has previously used Python major version %d, and it is now attempting to use Python major version %d."
+#~ msgstr "Ta sesja używała poprzednio Python w głównej wersji %d, teraz próbuje użyć Python w głównej wersji %d."
+
+#~ msgid "Start a new session to use a different Python major version."
+#~ msgstr "Uruchom nową sesję aby użyć innej głównej wersji Python."
+
+#~ msgid "plpy.prepare does not support composite types"
+#~ msgstr "plpy.prepare nie obsługuje typów złożonych"
+
+#~ msgid "PL/Python does not support conversion to arrays of row types."
+#~ msgstr "PL/Python nie obsługuje konwersji typów wierszowych na tablice."
+
+#~ msgid "unrecognized error in PLy_spi_execute_fetch_result"
+#~ msgstr "nierozpoznany błąd w PLy_spi_execute_fetch_result"
+
+#~ msgid "could not create new Python list"
+#~ msgstr "nie można utworzyć nowej listy Python"
+
+#~ msgid "PL/Python only supports one-dimensional arrays."
+#~ msgstr "PL/Python obsługuje tylko jednowymiarowe tablice."
+
+#~ msgid "cannot convert multidimensional array to Python list"
+#~ msgstr "nie można skonwertować tablicy wielowymiarowej na listę Python"
+
+#~ msgid "could not create the base SPI exceptions"
+#~ msgstr "nie można stworzyć bazowych wyjątków SPI"
+
+#~ msgid "plan.status takes no arguments"
+#~ msgstr "plan.status nie przyjmuje żadnych argumentów"
diff --git a/src/pl/plpython/po/pt_BR.po b/src/pl/plpython/po/pt_BR.po
new file mode 100644
index 0000000..ddeae15
--- /dev/null
+++ b/src/pl/plpython/po/pt_BR.po
@@ -0,0 +1,461 @@
+# Brazilian Portuguese message translation file for plpython
+#
+# Copyright (C) 2009-2022 PostgreSQL Global Development Group
+# This file is distributed under the same license as the PostgreSQL package.
+#
+# Euler Taveira <euler@eulerto.com>, 2009-2022.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: PostgreSQL 15\n"
+"Report-Msgid-Bugs-To: pgsql-bugs@lists.postgresql.org\n"
+"POT-Creation-Date: 2022-09-27 13:15-0300\n"
+"PO-Revision-Date: 2009-05-10 01:15-0300\n"
+"Last-Translator: Euler Taveira <euler@eulerto.com>\n"
+"Language-Team: Brazilian Portuguese <pgsql-translators@postgresql.org>\n"
+"Language: pt_BR\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=(n>1);\n"
+
+#: plpy_cursorobject.c:72
+#, c-format
+msgid "plpy.cursor expected a query or a plan"
+msgstr "plpy.cursor esperava uma consulta ou um plano"
+
+#: plpy_cursorobject.c:155
+#, c-format
+msgid "plpy.cursor takes a sequence as its second argument"
+msgstr "plpy.cursor tem uma sequência como seu segundo argumento"
+
+#: plpy_cursorobject.c:171 plpy_spi.c:205
+#, c-format
+msgid "could not execute plan"
+msgstr "não pôde executar plano"
+
+#: plpy_cursorobject.c:174 plpy_spi.c:208
+#, c-format
+msgid "Expected sequence of %d argument, got %d: %s"
+msgid_plural "Expected sequence of %d arguments, got %d: %s"
+msgstr[0] "Sequência esperada de %d argumento, recebeu %d: %s"
+msgstr[1] "Sequência esperada de %d argumentos, recebeu %d: %s"
+
+#: plpy_cursorobject.c:321
+#, c-format
+msgid "iterating a closed cursor"
+msgstr "iterando um cursor fechado"
+
+#: plpy_cursorobject.c:329 plpy_cursorobject.c:395
+#, c-format
+msgid "iterating a cursor in an aborted subtransaction"
+msgstr "iterando um cursor em uma subtransação abortada"
+
+#: plpy_cursorobject.c:387
+#, c-format
+msgid "fetch from a closed cursor"
+msgstr "busca em um cursor fechado"
+
+#: plpy_cursorobject.c:430 plpy_spi.c:401
+#, c-format
+msgid "query result has too many rows to fit in a Python list"
+msgstr "resultado da consulta tem muitos registros para caber em uma lista Python"
+
+#: plpy_cursorobject.c:482
+#, c-format
+msgid "closing a cursor in an aborted subtransaction"
+msgstr "fechando um cursor em uma subtransação abortada"
+
+#: plpy_elog.c:125 plpy_elog.c:126 plpy_plpymodule.c:530
+#, c-format
+msgid "%s"
+msgstr "%s"
+
+#: plpy_exec.c:139
+#, c-format
+msgid "unsupported set function return mode"
+msgstr "modo de retorno da função que retorna conjunto não é suportado"
+
+#: plpy_exec.c:140
+#, c-format
+msgid "PL/Python set-returning functions only support returning one value per call."
+msgstr "funções PL/Python que retornam conjunto só suportam retornar um valor por chamada."
+
+#: plpy_exec.c:153
+#, c-format
+msgid "returned object cannot be iterated"
+msgstr "objeto retornado não pode ser iterado"
+
+#: plpy_exec.c:154
+#, c-format
+msgid "PL/Python set-returning functions must return an iterable object."
+msgstr "funções PL/Python que retornam conjunto devem retornar um objeto iterável."
+
+#: plpy_exec.c:168
+#, c-format
+msgid "error fetching next item from iterator"
+msgstr "erro ao buscar próximo item do iterador"
+
+#: plpy_exec.c:211
+#, c-format
+msgid "PL/Python procedure did not return None"
+msgstr "procedimento PL/Python não retornou None"
+
+#: plpy_exec.c:215
+#, c-format
+msgid "PL/Python function with return type \"void\" did not return None"
+msgstr "função PL/Python com tipo de retorno \"void\" não retornou None"
+
+#: plpy_exec.c:369 plpy_exec.c:395
+#, c-format
+msgid "unexpected return value from trigger procedure"
+msgstr "função de gatilho retornou valor inesperado"
+
+#: plpy_exec.c:370
+#, c-format
+msgid "Expected None or a string."
+msgstr "None ou uma cadeia de caracteres era esperado."
+
+#: plpy_exec.c:385
+#, c-format
+msgid "PL/Python trigger function returned \"MODIFY\" in a DELETE trigger -- ignored"
+msgstr "função de gatilho PL/Python retornou \"MODIFY\" em um gatilho DELETE -- ignorado"
+
+#: plpy_exec.c:396
+#, c-format
+msgid "Expected None, \"OK\", \"SKIP\", or \"MODIFY\"."
+msgstr "Era esperado None, \"OK\", \"SKIP\" ou \"MODIFY\"."
+
+#: plpy_exec.c:441
+#, c-format
+msgid "PyList_SetItem() failed, while setting up arguments"
+msgstr "PyList_SetItem() falhou ao definir argumentos"
+
+#: plpy_exec.c:445
+#, c-format
+msgid "PyDict_SetItemString() failed, while setting up arguments"
+msgstr "PyDict_SetItemString() falhou ao definir argumentos"
+
+#: plpy_exec.c:457
+#, c-format
+msgid "function returning record called in context that cannot accept type record"
+msgstr "função que retorna record foi chamada em um contexto que não pode aceitar tipo record"
+
+#: plpy_exec.c:674
+#, c-format
+msgid "while creating return value"
+msgstr "ao criar valor de retorno"
+
+#: plpy_exec.c:908
+#, c-format
+msgid "TD[\"new\"] deleted, cannot modify row"
+msgstr "TD[\"new\"] removido, não pode modificar registro"
+
+#: plpy_exec.c:913
+#, c-format
+msgid "TD[\"new\"] is not a dictionary"
+msgstr "TD[\"new\"] não é um dicionário"
+
+#: plpy_exec.c:938
+#, c-format
+msgid "TD[\"new\"] dictionary key at ordinal position %d is not a string"
+msgstr "chave do dicionário TD[\"new\"] na posição %d não é uma cadeia de caracteres"
+
+#: plpy_exec.c:945
+#, c-format
+msgid "key \"%s\" found in TD[\"new\"] does not exist as a column in the triggering row"
+msgstr "chave \"%s\" encontrada em TD[\"new\"] não existe como uma coluna no registro do gatilho"
+
+#: plpy_exec.c:950
+#, c-format
+msgid "cannot set system attribute \"%s\""
+msgstr "não pode definir atributo do sistema \"%s\""
+
+#: plpy_exec.c:955
+#, c-format
+msgid "cannot set generated column \"%s\""
+msgstr "não pode definir coluna gerada \"%s\""
+
+#: plpy_exec.c:1013
+#, c-format
+msgid "while modifying trigger row"
+msgstr "ao modificar registro de gatilho"
+
+#: plpy_exec.c:1071
+#, c-format
+msgid "forcibly aborting a subtransaction that has not been exited"
+msgstr "forçado a abortar subtransação que não foi concluída"
+
+#: plpy_main.c:111
+#, c-format
+msgid "multiple Python libraries are present in session"
+msgstr "múltiplas bibliotecas do Python estão presentes na sessão"
+
+#: plpy_main.c:112
+#, c-format
+msgid "Only one Python major version can be used in one session."
+msgstr "Apenas uma versão do Python pode ser utilizada na sessão."
+
+#: plpy_main.c:124
+#, c-format
+msgid "untrapped error in initialization"
+msgstr "erro não interceptado na inicialização"
+
+#: plpy_main.c:147
+#, c-format
+msgid "could not import \"__main__\" module"
+msgstr "não pôde importar módulo \"__main__\""
+
+#: plpy_main.c:156
+#, c-format
+msgid "could not initialize globals"
+msgstr "não pôde inicializar globais"
+
+#: plpy_main.c:354
+#, c-format
+msgid "PL/Python procedure \"%s\""
+msgstr "procedimento PL/Python \"%s\""
+
+#: plpy_main.c:357
+#, c-format
+msgid "PL/Python function \"%s\""
+msgstr "função PL/Python \"%s\""
+
+#: plpy_main.c:365
+#, c-format
+msgid "PL/Python anonymous code block"
+msgstr "bloco de código PL/Python anônimo"
+
+#: plpy_plpymodule.c:168 plpy_plpymodule.c:171
+#, c-format
+msgid "could not import \"plpy\" module"
+msgstr "não pôde importar módulo \"plpy\""
+
+#: plpy_plpymodule.c:182
+#, c-format
+msgid "could not create the spiexceptions module"
+msgstr "não pôde criar o módulo spiexceptions"
+
+#: plpy_plpymodule.c:190
+#, c-format
+msgid "could not add the spiexceptions module"
+msgstr "não pôde adicionar o módulo spiexceptions"
+
+#: plpy_plpymodule.c:257
+#, c-format
+msgid "could not generate SPI exceptions"
+msgstr "não pôde gerar exceções da SPI"
+
+#: plpy_plpymodule.c:425
+#, c-format
+msgid "could not unpack arguments in plpy.elog"
+msgstr "não pode desempacotar argumentos em plpy.elog"
+
+#: plpy_plpymodule.c:434
+msgid "could not parse error message in plpy.elog"
+msgstr "não pode analisar mensagem de erro em plpy.elog"
+
+#: plpy_plpymodule.c:451
+#, c-format
+msgid "argument 'message' given by name and position"
+msgstr "argumento 'message' informado por nome e posição"
+
+#: plpy_plpymodule.c:478
+#, c-format
+msgid "'%s' is an invalid keyword argument for this function"
+msgstr "'%s' é um argumento inválido para esta função"
+
+#: plpy_plpymodule.c:489 plpy_plpymodule.c:495
+#, c-format
+msgid "invalid SQLSTATE code"
+msgstr "código SQLSTATE inválido"
+
+#: plpy_procedure.c:225
+#, c-format
+msgid "trigger functions can only be called as triggers"
+msgstr "funções de gatilho só podem ser chamadas como gatilhos"
+
+#: plpy_procedure.c:229
+#, c-format
+msgid "PL/Python functions cannot return type %s"
+msgstr "funções PL/Python não podem retornar tipo %s"
+
+#: plpy_procedure.c:307
+#, c-format
+msgid "PL/Python functions cannot accept type %s"
+msgstr "funções PL/Python não podem aceitar tipo %s"
+
+#: plpy_procedure.c:397
+#, c-format
+msgid "could not compile PL/Python function \"%s\""
+msgstr "não pôde compilar função PL/Python \"%s\""
+
+#: plpy_procedure.c:400
+#, c-format
+msgid "could not compile anonymous PL/Python code block"
+msgstr "não pôde compilar bloco de código PL/Python anônimo"
+
+#: plpy_resultobject.c:117 plpy_resultobject.c:143 plpy_resultobject.c:169
+#, c-format
+msgid "command did not produce a result set"
+msgstr "comando não produziu um conjunto de resultados"
+
+#: plpy_spi.c:56
+#, c-format
+msgid "second argument of plpy.prepare must be a sequence"
+msgstr "segundo argumento de plpy.prepare deve ser uma sequência"
+
+#: plpy_spi.c:98
+#, c-format
+msgid "plpy.prepare: type name at ordinal position %d is not a string"
+msgstr "plpy.prepare: nome do tipo na posição %d não é uma cadeia de caracteres"
+
+#: plpy_spi.c:170
+#, c-format
+msgid "plpy.execute expected a query or a plan"
+msgstr "plpy.execute espera uma consulta ou um plano"
+
+#: plpy_spi.c:189
+#, c-format
+msgid "plpy.execute takes a sequence as its second argument"
+msgstr "plpy.execute recebe uma sequência como segundo argumento"
+
+#: plpy_spi.c:297
+#, c-format
+msgid "SPI_execute_plan failed: %s"
+msgstr "SPI_execute_plan falhou: %s"
+
+#: plpy_spi.c:339
+#, c-format
+msgid "SPI_execute failed: %s"
+msgstr "SPI_execute falhou: %s"
+
+#: plpy_subxactobject.c:92
+#, c-format
+msgid "this subtransaction has already been entered"
+msgstr "essa subtransação já foi iniciada"
+
+#: plpy_subxactobject.c:98 plpy_subxactobject.c:156
+#, c-format
+msgid "this subtransaction has already been exited"
+msgstr "essa subtransação já foi concluída"
+
+#: plpy_subxactobject.c:150
+#, c-format
+msgid "this subtransaction has not been entered"
+msgstr "essa subtransação não foi iniciada"
+
+#: plpy_subxactobject.c:162
+#, c-format
+msgid "there is no subtransaction to exit from"
+msgstr "não há uma subtransação a ser concluída"
+
+#: plpy_typeio.c:587
+#, c-format
+msgid "could not import a module for Decimal constructor"
+msgstr "não pôde importar módulo para construtor Decimal"
+
+#: plpy_typeio.c:591
+#, c-format
+msgid "no Decimal attribute in module"
+msgstr "nenhum atributo Decimal no módulo"
+
+#: plpy_typeio.c:597
+#, c-format
+msgid "conversion from numeric to Decimal failed"
+msgstr "conversão de numeric para Decimal falhou"
+
+#: plpy_typeio.c:911
+#, c-format
+msgid "could not create bytes representation of Python object"
+msgstr "não pôde criar representação de bytes de um objeto Python"
+
+#: plpy_typeio.c:1048
+#, c-format
+msgid "could not create string representation of Python object"
+msgstr "não pôde criar representação de cadeia de caracteres de um objeto Python"
+
+#: plpy_typeio.c:1059
+#, c-format
+msgid "could not convert Python object into cstring: Python string representation appears to contain null bytes"
+msgstr "não pôde converter objeto Python em cstring: representação de cadeia de caracteres Python parece conter bytes nulos"
+
+#: plpy_typeio.c:1170
+#, c-format
+msgid "number of array dimensions exceeds the maximum allowed (%d)"
+msgstr "número de dimensões da matriz excede o máximo permitido (%d)"
+
+#: plpy_typeio.c:1175
+#, c-format
+msgid "could not determine sequence length for function return value"
+msgstr "não pôde determinar tamanho da sequência para valor de retorno da função"
+
+#: plpy_typeio.c:1180 plpy_typeio.c:1186
+#, c-format
+msgid "array size exceeds the maximum allowed"
+msgstr "tamanho da matriz excede o máximo permitido"
+
+#: plpy_typeio.c:1214
+#, c-format
+msgid "return value of function with array return type is not a Python sequence"
+msgstr "valor de retorno da função do tipo matriz retorna tipo que não é uma sequência Python"
+
+#: plpy_typeio.c:1261
+#, c-format
+msgid "wrong length of inner sequence: has length %d, but %d was expected"
+msgstr "tamanho incorreto da sequência interna: tem tamanho %d, mas %d era esperado"
+
+#: plpy_typeio.c:1263
+#, c-format
+msgid "To construct a multidimensional array, the inner sequences must all have the same length."
+msgstr "Para construir uma matriz multidimensional, todas as sequências internas devem ter o mesmo tamanho."
+
+#: plpy_typeio.c:1342
+#, c-format
+msgid "malformed record literal: \"%s\""
+msgstr "matriz mal formada: \"%s\""
+
+#: plpy_typeio.c:1343
+#, c-format
+msgid "Missing left parenthesis."
+msgstr "Faltando parêntese esquerdo."
+
+#: plpy_typeio.c:1344 plpy_typeio.c:1545
+#, c-format
+msgid "To return a composite type in an array, return the composite type as a Python tuple, e.g., \"[('foo',)]\"."
+msgstr "Para retornar um tipo composto em uma matriz, retorne o tipo composto como uma tupla do Python, i.e., \"[('foo',)]\"."
+
+#: plpy_typeio.c:1391
+#, c-format
+msgid "key \"%s\" not found in mapping"
+msgstr "chave \"%s\" não foi encontrada no mapeamento"
+
+#: plpy_typeio.c:1392
+#, c-format
+msgid "To return null in a column, add the value None to the mapping with the key named after the column."
+msgstr "Para retornar nulo em uma coluna, adicionar o valor None no mapeamento cuja chave é o nome da coluna."
+
+#: plpy_typeio.c:1445
+#, c-format
+msgid "length of returned sequence did not match number of columns in row"
+msgstr "tamanho da sequência retornada não combina com número de colunas no registro"
+
+#: plpy_typeio.c:1543
+#, c-format
+msgid "attribute \"%s\" does not exist in Python object"
+msgstr "atributo \"%s\" não existe no objeto Python"
+
+#: plpy_typeio.c:1546
+#, c-format
+msgid "To return null in a column, let the returned object have an attribute named after column with value None."
+msgstr "Para retornar nulo na coluna, deixe o objeto retornado ter um atributo cuja chave é o nome do coluna e o valor é None."
+
+#: plpy_util.c:31
+#, c-format
+msgid "could not convert Python Unicode object to bytes"
+msgstr "não pôde converter objeto Unicode Python para bytes"
+
+#: plpy_util.c:37
+#, c-format
+msgid "could not extract bytes from encoded string"
+msgstr "não pôde extrair bytes de cadeia de caracteres codificada"
diff --git a/src/pl/plpython/po/ru.po b/src/pl/plpython/po/ru.po
new file mode 100644
index 0000000..b3a6d40
--- /dev/null
+++ b/src/pl/plpython/po/ru.po
@@ -0,0 +1,561 @@
+# Russian message translation file for plpython
+# Copyright (C) 2012-2016 PostgreSQL Global Development Group
+# This file is distributed under the same license as the PostgreSQL package.
+# Alexander Lakhin <exclusion@gmail.com>, 2012-2017, 2018, 2019.
+msgid ""
+msgstr ""
+"Project-Id-Version: plpython (PostgreSQL current)\n"
+"Report-Msgid-Bugs-To: pgsql-bugs@lists.postgresql.org\n"
+"POT-Creation-Date: 2023-11-03 09:08+0300\n"
+"PO-Revision-Date: 2019-08-29 15:42+0300\n"
+"Last-Translator: Alexander Lakhin <exclusion@gmail.com>\n"
+"Language-Team: Russian <pgsql-ru-general@postgresql.org>\n"
+"Language: ru\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && "
+"n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n"
+
+#: plpy_cursorobject.c:72
+#, c-format
+msgid "plpy.cursor expected a query or a plan"
+msgstr "plpy.cursor ожидает запрос или план"
+
+#: plpy_cursorobject.c:155
+#, c-format
+msgid "plpy.cursor takes a sequence as its second argument"
+msgstr "plpy.cursor принимает в качестве второго аргумента последовательность"
+
+#: plpy_cursorobject.c:171 plpy_spi.c:205
+#, c-format
+msgid "could not execute plan"
+msgstr "нельзя выполнить план"
+
+#: plpy_cursorobject.c:174 plpy_spi.c:208
+#, c-format
+msgid "Expected sequence of %d argument, got %d: %s"
+msgid_plural "Expected sequence of %d arguments, got %d: %s"
+msgstr[0] "Ожидалась последовательность из %d аргумента, получено %d: %s"
+msgstr[1] "Ожидалась последовательность из %d аргументов, получено %d: %s"
+msgstr[2] "Ожидалась последовательность из %d аргументов, получено %d: %s"
+
+#: plpy_cursorobject.c:321
+#, c-format
+msgid "iterating a closed cursor"
+msgstr "перемещение закрытого курсора"
+
+#: plpy_cursorobject.c:329 plpy_cursorobject.c:395
+#, c-format
+msgid "iterating a cursor in an aborted subtransaction"
+msgstr "перемещение курсора в прерванной подтранзакции"
+
+#: plpy_cursorobject.c:387
+#, c-format
+msgid "fetch from a closed cursor"
+msgstr "выборка из закрытого курсора"
+
+#: plpy_cursorobject.c:430 plpy_spi.c:401
+#, c-format
+msgid "query result has too many rows to fit in a Python list"
+msgstr ""
+"результат запроса содержит слишком много строк для передачи в списке Python"
+
+#: plpy_cursorobject.c:482
+#, c-format
+msgid "closing a cursor in an aborted subtransaction"
+msgstr "закрытие курсора в прерванной подтранзакции"
+
+#: plpy_elog.c:122 plpy_elog.c:123 plpy_plpymodule.c:530
+#, c-format
+msgid "%s"
+msgstr "%s"
+
+#: plpy_exec.c:139
+#, c-format
+msgid "unsupported set function return mode"
+msgstr "неподдерживаемый режим возврата для функции с результатом-множеством"
+
+#: plpy_exec.c:140
+#, c-format
+msgid ""
+"PL/Python set-returning functions only support returning one value per call."
+msgstr ""
+"Функции PL/Python с результатом-множеством могут возвращать только одно "
+"значение за вызов."
+
+#: plpy_exec.c:153
+#, c-format
+msgid "returned object cannot be iterated"
+msgstr "возвращаемый объект не поддерживает итерации"
+
+#: plpy_exec.c:154
+#, c-format
+msgid "PL/Python set-returning functions must return an iterable object."
+msgstr ""
+"Функции PL/Python с результатом-множеством должны возвращать объекты с "
+"возможностью итерации."
+
+#: plpy_exec.c:168
+#, c-format
+msgid "error fetching next item from iterator"
+msgstr "ошибка получения следующего элемента из итератора"
+
+#: plpy_exec.c:211
+#, c-format
+msgid "PL/Python procedure did not return None"
+msgstr "процедура PL/Python вернула не None"
+
+#: plpy_exec.c:215
+#, c-format
+msgid "PL/Python function with return type \"void\" did not return None"
+msgstr "функция PL/Python с типом результата \"void\" вернула не None"
+
+#: plpy_exec.c:369 plpy_exec.c:393
+#, c-format
+msgid "unexpected return value from trigger procedure"
+msgstr "триггерная процедура вернула недопустимое значение"
+
+#: plpy_exec.c:370
+#, c-format
+msgid "Expected None or a string."
+msgstr "Ожидалось None или строка."
+
+#: plpy_exec.c:383
+#, c-format
+msgid ""
+"PL/Python trigger function returned \"MODIFY\" in a DELETE trigger -- ignored"
+msgstr ""
+"триггерная функция PL/Python вернула \"MODIFY\" в триггере DELETE -- "
+"игнорируется"
+
+#: plpy_exec.c:394
+#, c-format
+msgid "Expected None, \"OK\", \"SKIP\", or \"MODIFY\"."
+msgstr "Ожидалось None, \"OK\", \"SKIP\" или \"MODIFY\"."
+
+#: plpy_exec.c:444
+#, c-format
+msgid "PyList_SetItem() failed, while setting up arguments"
+msgstr "ошибка в PyList_SetItem() при настройке аргументов"
+
+#: plpy_exec.c:448
+#, c-format
+msgid "PyDict_SetItemString() failed, while setting up arguments"
+msgstr "ошибка в PyDict_SetItemString() при настройке аргументов"
+
+#: plpy_exec.c:460
+#, c-format
+msgid ""
+"function returning record called in context that cannot accept type record"
+msgstr ""
+"функция, возвращающая запись, вызвана в контексте, не допускающем этот тип"
+
+#: plpy_exec.c:677
+#, c-format
+msgid "while creating return value"
+msgstr "при создании возвращаемого значения"
+
+#: plpy_exec.c:924
+#, c-format
+msgid "TD[\"new\"] deleted, cannot modify row"
+msgstr "элемент TD[\"new\"] удалён -- изменить строку нельзя"
+
+#: plpy_exec.c:929
+#, c-format
+msgid "TD[\"new\"] is not a dictionary"
+msgstr "TD[\"new\"] - не словарь"
+
+#: plpy_exec.c:954
+#, c-format
+msgid "TD[\"new\"] dictionary key at ordinal position %d is not a string"
+msgstr "ключ словаря TD[\"new\"] с порядковым номером %d не является строкой"
+
+#: plpy_exec.c:961
+#, c-format
+msgid ""
+"key \"%s\" found in TD[\"new\"] does not exist as a column in the triggering "
+"row"
+msgstr ""
+"ключу \"%s\", найденному в TD[\"new\"], не соответствует столбец в строке, "
+"обрабатываемой триггером"
+
+#: plpy_exec.c:966
+#, c-format
+msgid "cannot set system attribute \"%s\""
+msgstr "присвоить значение системному атрибуту \"%s\" нельзя"
+
+#: plpy_exec.c:971
+#, c-format
+msgid "cannot set generated column \"%s\""
+msgstr "присвоить значение генерируемому столбцу \"%s\" нельзя"
+
+#: plpy_exec.c:1029
+#, c-format
+msgid "while modifying trigger row"
+msgstr "при изменении строки в триггере"
+
+#: plpy_exec.c:1087
+#, c-format
+msgid "forcibly aborting a subtransaction that has not been exited"
+msgstr "принудительное прерывание незавершённой подтранзакции"
+
+#: plpy_main.c:109
+#, c-format
+msgid "multiple Python libraries are present in session"
+msgstr "в сеансе представлено несколько библиотек Python"
+
+#: plpy_main.c:110
+#, c-format
+msgid "Only one Python major version can be used in one session."
+msgstr "В одном сеансе нельзя использовать Python разных старших версий."
+
+#: plpy_main.c:122
+#, c-format
+msgid "untrapped error in initialization"
+msgstr "необработанная ошибка при инициализации"
+
+#: plpy_main.c:145
+#, c-format
+msgid "could not import \"__main__\" module"
+msgstr "не удалось импортировать модуль \"__main__\""
+
+#: plpy_main.c:154
+#, c-format
+msgid "could not initialize globals"
+msgstr "не удалось инициализировать глобальные данные"
+
+#: plpy_main.c:352
+#, c-format
+msgid "PL/Python procedure \"%s\""
+msgstr "процедура PL/Python \"%s\""
+
+#: plpy_main.c:355
+#, c-format
+msgid "PL/Python function \"%s\""
+msgstr "функция PL/Python \"%s\""
+
+#: plpy_main.c:363
+#, c-format
+msgid "PL/Python anonymous code block"
+msgstr "анонимный блок кода PL/Python"
+
+#: plpy_plpymodule.c:168 plpy_plpymodule.c:171
+#, c-format
+msgid "could not import \"plpy\" module"
+msgstr "не удалось импортировать модуль \"plpy\""
+
+#: plpy_plpymodule.c:182
+#, c-format
+msgid "could not create the spiexceptions module"
+msgstr "не удалось создать модуль spiexceptions"
+
+#: plpy_plpymodule.c:190
+#, c-format
+msgid "could not add the spiexceptions module"
+msgstr "не удалось добавить модуль spiexceptions"
+
+#: plpy_plpymodule.c:257
+#, c-format
+msgid "could not generate SPI exceptions"
+msgstr "не удалось сгенерировать исключения SPI"
+
+#: plpy_plpymodule.c:425
+#, c-format
+msgid "could not unpack arguments in plpy.elog"
+msgstr "не удалось распаковать аргументы в plpy.elog"
+
+#: plpy_plpymodule.c:434
+msgid "could not parse error message in plpy.elog"
+msgstr "не удалось разобрать сообщение об ошибке в plpy.elog"
+
+#: plpy_plpymodule.c:451
+#, c-format
+msgid "argument 'message' given by name and position"
+msgstr "аргумент 'message' задан и по имени, и по позиции"
+
+#: plpy_plpymodule.c:478
+#, c-format
+msgid "'%s' is an invalid keyword argument for this function"
+msgstr "'%s' - недопустимое ключевое слово (аргумент) для этой функции"
+
+#: plpy_plpymodule.c:489 plpy_plpymodule.c:495
+#, c-format
+msgid "invalid SQLSTATE code"
+msgstr "неверный код SQLSTATE"
+
+#: plpy_procedure.c:225
+#, c-format
+msgid "trigger functions can only be called as triggers"
+msgstr "триггерные функции могут вызываться только в триггерах"
+
+#: plpy_procedure.c:229
+#, c-format
+msgid "PL/Python functions cannot return type %s"
+msgstr "функции PL/Python не могут возвращать тип %s"
+
+#: plpy_procedure.c:307
+#, c-format
+msgid "PL/Python functions cannot accept type %s"
+msgstr "функции PL/Python не могут принимать тип %s"
+
+#: plpy_procedure.c:395
+#, c-format
+msgid "could not compile PL/Python function \"%s\""
+msgstr "не удалось скомпилировать функцию PL/Python \"%s\""
+
+#: plpy_procedure.c:398
+#, c-format
+msgid "could not compile anonymous PL/Python code block"
+msgstr "не удалось скомпилировать анонимный блок кода PL/Python"
+
+#: plpy_resultobject.c:117 plpy_resultobject.c:143 plpy_resultobject.c:169
+#, c-format
+msgid "command did not produce a result set"
+msgstr "команда не выдала результирующий набор"
+
+#: plpy_spi.c:56
+#, c-format
+msgid "second argument of plpy.prepare must be a sequence"
+msgstr "вторым аргументом plpy.prepare должна быть последовательность"
+
+#: plpy_spi.c:98
+#, c-format
+msgid "plpy.prepare: type name at ordinal position %d is not a string"
+msgstr "plpy.prepare: имя типа с порядковым номером %d не является строкой"
+
+#: plpy_spi.c:170
+#, c-format
+msgid "plpy.execute expected a query or a plan"
+msgstr "plpy.execute ожидает запрос или план"
+
+#: plpy_spi.c:189
+#, c-format
+msgid "plpy.execute takes a sequence as its second argument"
+msgstr "plpy.execute принимает в качестве второго аргумента последовательность"
+
+#: plpy_spi.c:297
+#, c-format
+msgid "SPI_execute_plan failed: %s"
+msgstr "ошибка в SPI_execute_plan: %s"
+
+#: plpy_spi.c:339
+#, c-format
+msgid "SPI_execute failed: %s"
+msgstr "ошибка в SPI_execute: %s"
+
+#: plpy_subxactobject.c:92
+#, c-format
+msgid "this subtransaction has already been entered"
+msgstr "эта подтранзакция уже начата"
+
+#: plpy_subxactobject.c:98 plpy_subxactobject.c:156
+#, c-format
+msgid "this subtransaction has already been exited"
+msgstr "эта подтранзакция уже закончена"
+
+#: plpy_subxactobject.c:150
+#, c-format
+msgid "this subtransaction has not been entered"
+msgstr "эта подтранзакция ещё не начата"
+
+#: plpy_subxactobject.c:162
+#, c-format
+msgid "there is no subtransaction to exit from"
+msgstr "нет подтранзакции, которую нужно закончить"
+
+#: plpy_typeio.c:588
+#, c-format
+msgid "could not import a module for Decimal constructor"
+msgstr "не удалось импортировать модуль для конструктора Decimal"
+
+#: plpy_typeio.c:592
+#, c-format
+msgid "no Decimal attribute in module"
+msgstr "в модуле нет атрибута Decimal"
+
+#: plpy_typeio.c:598
+#, c-format
+msgid "conversion from numeric to Decimal failed"
+msgstr "не удалось преобразовать numeric в Decimal"
+
+#: plpy_typeio.c:912
+#, c-format
+msgid "could not create bytes representation of Python object"
+msgstr "не удалось создать байтовое представление объекта Python"
+
+#: plpy_typeio.c:1049
+#, c-format
+msgid "could not create string representation of Python object"
+msgstr "не удалось создать строковое представление объекта Python"
+
+#: plpy_typeio.c:1060
+#, c-format
+msgid ""
+"could not convert Python object into cstring: Python string representation "
+"appears to contain null bytes"
+msgstr ""
+"не удалось преобразовать объект Python в cstring: похоже, представление "
+"строки Python содержит нулевые байты"
+
+#: plpy_typeio.c:1157
+#, c-format
+msgid ""
+"return value of function with array return type is not a Python sequence"
+msgstr ""
+"возвращаемое значение функции с результатом-массивом не является "
+"последовательностью"
+
+#: plpy_typeio.c:1202
+#, c-format
+msgid "could not determine sequence length for function return value"
+msgstr ""
+"не удалось определить длину последовательности в возвращаемом функцией "
+"значении"
+
+#: plpy_typeio.c:1222 plpy_typeio.c:1237 plpy_typeio.c:1253
+#, c-format
+msgid ""
+"multidimensional arrays must have array expressions with matching dimensions"
+msgstr ""
+"для многомерных массивов должны задаваться выражения с соответствующими "
+"размерностями"
+
+#: plpy_typeio.c:1227
+#, c-format
+msgid "number of array dimensions exceeds the maximum allowed (%d)"
+msgstr "число размерностей массива превышает предел (%d)"
+
+#: plpy_typeio.c:1329
+#, c-format
+msgid "malformed record literal: \"%s\""
+msgstr "ошибка в литерале записи: \"%s\""
+
+#: plpy_typeio.c:1330
+#, c-format
+msgid "Missing left parenthesis."
+msgstr "Отсутствует левая скобка."
+
+#: plpy_typeio.c:1331 plpy_typeio.c:1532
+#, c-format
+msgid ""
+"To return a composite type in an array, return the composite type as a "
+"Python tuple, e.g., \"[('foo',)]\"."
+msgstr ""
+"Чтобы возвратить составной тип в массиве, нужно возвратить составное "
+"значение в виде кортежа Python, например: \"[('foo',)]\"."
+
+#: plpy_typeio.c:1378
+#, c-format
+msgid "key \"%s\" not found in mapping"
+msgstr "ключ \"%s\" не найден в сопоставлении"
+
+#: plpy_typeio.c:1379
+#, c-format
+msgid ""
+"To return null in a column, add the value None to the mapping with the key "
+"named after the column."
+msgstr ""
+"Чтобы присвоить столбцу NULL, добавьте в сопоставление значение None с "
+"ключом-именем столбца."
+
+#: plpy_typeio.c:1432
+#, c-format
+msgid "length of returned sequence did not match number of columns in row"
+msgstr "длина возвращённой последовательности не равна числу столбцов в строке"
+
+#: plpy_typeio.c:1530
+#, c-format
+msgid "attribute \"%s\" does not exist in Python object"
+msgstr "в объекте Python не существует атрибут \"%s\""
+
+#: plpy_typeio.c:1533
+#, c-format
+msgid ""
+"To return null in a column, let the returned object have an attribute named "
+"after column with value None."
+msgstr ""
+"Чтобы присвоить столбцу NULL, присвойте возвращаемому значению атрибут с "
+"именем столбца и значением None."
+
+#: plpy_util.c:31
+#, c-format
+msgid "could not convert Python Unicode object to bytes"
+msgstr "не удалось преобразовать объект Python Unicode в байты"
+
+#: plpy_util.c:37
+#, c-format
+msgid "could not extract bytes from encoded string"
+msgstr "не удалось извлечь байты из кодированной строки"
+
+#, c-format
+#~ msgid "array size exceeds the maximum allowed"
+#~ msgstr "размер массива превышает предел"
+
+#, c-format
+#~ msgid "wrong length of inner sequence: has length %d, but %d was expected"
+#~ msgstr "неверная длина внутренней последовательности: %d (ожидалось: %d)"
+
+#, c-format
+#~ msgid ""
+#~ "To construct a multidimensional array, the inner sequences must all have "
+#~ "the same length."
+#~ msgstr ""
+#~ "Для образования многомерного массива внутренние последовательности должны "
+#~ "иметь одинаковую длину."
+
+#~ msgid "could not create new dictionary while building trigger arguments"
+#~ msgstr "не удалось создать словарь для передачи аргументов триггера"
+
+#~ msgid "could not create globals"
+#~ msgstr "не удалось создать глобальные данные"
+
+#~ msgid "could not create exception \"%s\""
+#~ msgstr "не удалось сгенерировать исключение \"%s\""
+
+#~ msgid "could not create new dictionary"
+#~ msgstr "не удалось создать словарь"
+
+#~ msgid "plan.status takes no arguments"
+#~ msgstr "plan.status не принимает аргументы"
+
+#~ msgid "cannot convert multidimensional array to Python list"
+#~ msgstr "преобразовать многомерный массив в список Python нельзя"
+
+#~ msgid "PL/Python only supports one-dimensional arrays."
+#~ msgstr "PL/Python поддерживает только одномерные массивы."
+
+#~ msgid "could not create new Python list"
+#~ msgstr "не удалось создать список Python"
+
+#~ msgid "could not create the base SPI exceptions"
+#~ msgstr "не удалось создать базовые объекты исключений SPI"
+
+#~ msgid "the message is already specified"
+#~ msgstr "сообщение уже указано"
+
+#~ msgid "Python major version mismatch in session"
+#~ msgstr "несовпадение базовой версии Python в сеансе"
+
+#~ msgid ""
+#~ "This session has previously used Python major version %d, and it is now "
+#~ "attempting to use Python major version %d."
+#~ msgstr ""
+#~ "В данном сеансе до этого использовался Python базовой версии %d, а сейчас "
+#~ "планируется использовать Python версии %d."
+
+#~ msgid "Start a new session to use a different Python major version."
+#~ msgstr ""
+#~ "Чтобы переключиться на другую базовую версию Python, начните новый сеанс."
+
+#~ msgid "plpy.prepare does not support composite types"
+#~ msgstr "plpy.prepare не поддерживает составные типы"
+
+#~ msgid "PL/Python does not support conversion to arrays of row types."
+#~ msgstr "PL/Python не поддерживает преобразование в массивы кортежей."
+
+#~ msgid "could not initialize plpy"
+#~ msgstr "не удалось инициализировать plpy"
+
+#~ msgid "unrecognized error in PLy_spi_execute_fetch_result"
+#~ msgstr "нераспознанная ошибка в PLy_spi_execute_fetch_result"
diff --git a/src/pl/plpython/po/sv.po b/src/pl/plpython/po/sv.po
new file mode 100644
index 0000000..8642c20
--- /dev/null
+++ b/src/pl/plpython/po/sv.po
@@ -0,0 +1,461 @@
+# Swedish message translation file for plpython
+# Copyright (C) 2017 PostgreSQL Global Development Group
+# This file is distributed under the same license as the PostgreSQL package.
+# Dennis Björklund <db@zigo.dhs.org>, 2017, 2018, 2019, 2020, 2021, 2022, 2023.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: PostgreSQL 16\n"
+"Report-Msgid-Bugs-To: pgsql-bugs@lists.postgresql.org\n"
+"POT-Creation-Date: 2023-08-02 08:08+0000\n"
+"PO-Revision-Date: 2023-08-02 12:03+0200\n"
+"Last-Translator: Dennis Björklund <db@zigo.dhs.org>\n"
+"Language-Team: Swedish <pgsql-translators@postgresql.org>\n"
+"Language: sv\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=n != 1;\n"
+
+#: plpy_cursorobject.c:72
+#, c-format
+msgid "plpy.cursor expected a query or a plan"
+msgstr "plpy.cursor förväntade sig en fråga eller en plan"
+
+#: plpy_cursorobject.c:155
+#, c-format
+msgid "plpy.cursor takes a sequence as its second argument"
+msgstr "plpy.cursor tar en sekvens som sitt andra argument"
+
+#: plpy_cursorobject.c:171 plpy_spi.c:205
+#, c-format
+msgid "could not execute plan"
+msgstr "kunde inte exekvera plan"
+
+#: plpy_cursorobject.c:174 plpy_spi.c:208
+#, c-format
+msgid "Expected sequence of %d argument, got %d: %s"
+msgid_plural "Expected sequence of %d arguments, got %d: %s"
+msgstr[0] "Förväntade sekvens med %d argument, fick %d: %s"
+msgstr[1] "Förväntade sekvens med %d argument, fick %d: %s"
+
+#: plpy_cursorobject.c:321
+#, c-format
+msgid "iterating a closed cursor"
+msgstr "itererar med en stängd markör"
+
+#: plpy_cursorobject.c:329 plpy_cursorobject.c:395
+#, c-format
+msgid "iterating a cursor in an aborted subtransaction"
+msgstr "itererar med en markör i en avbruten subtransaktion"
+
+#: plpy_cursorobject.c:387
+#, c-format
+msgid "fetch from a closed cursor"
+msgstr "hämta från en stängd markör"
+
+#: plpy_cursorobject.c:430 plpy_spi.c:401
+#, c-format
+msgid "query result has too many rows to fit in a Python list"
+msgstr "frågeresultet har för många rader för att få plats i en Python-lista"
+
+#: plpy_cursorobject.c:482
+#, c-format
+msgid "closing a cursor in an aborted subtransaction"
+msgstr "stänger en markör i en avbruten subtransaktion"
+
+#: plpy_elog.c:125 plpy_elog.c:126 plpy_plpymodule.c:530
+#, c-format
+msgid "%s"
+msgstr "%s"
+
+#: plpy_exec.c:139
+#, c-format
+msgid "unsupported set function return mode"
+msgstr "ej supportat returläge för mängdfunktion"
+
+#: plpy_exec.c:140
+#, c-format
+msgid "PL/Python set-returning functions only support returning one value per call."
+msgstr "PL/Python mängdreturnerande funktioner stöder bara ett värde per anrop."
+
+#: plpy_exec.c:153
+#, c-format
+msgid "returned object cannot be iterated"
+msgstr "returnerat objekt kan inte itereras"
+
+#: plpy_exec.c:154
+#, c-format
+msgid "PL/Python set-returning functions must return an iterable object."
+msgstr "PL/Python mängdreturnerande funktioner måste returnera ett itererbart objekt."
+
+#: plpy_exec.c:168
+#, c-format
+msgid "error fetching next item from iterator"
+msgstr "fel vid hämtning av nästa del från iteratorn"
+
+#: plpy_exec.c:211
+#, c-format
+msgid "PL/Python procedure did not return None"
+msgstr "PL/Python-procedur returnerade inte None"
+
+#: plpy_exec.c:215
+#, c-format
+msgid "PL/Python function with return type \"void\" did not return None"
+msgstr "PL/Python-funktion med returtyp \"void\" returnerade inte None"
+
+#: plpy_exec.c:369 plpy_exec.c:393
+#, c-format
+msgid "unexpected return value from trigger procedure"
+msgstr "oväntat returvärde från triggerprocedur"
+
+#: plpy_exec.c:370
+#, c-format
+msgid "Expected None or a string."
+msgstr "Förväntade None eller en sträng."
+
+#: plpy_exec.c:383
+#, c-format
+msgid "PL/Python trigger function returned \"MODIFY\" in a DELETE trigger -- ignored"
+msgstr "PL/Python-triggerfunktion returnerade \"MODIFY\" i en DELETE-trigger -- ignorerad"
+
+#: plpy_exec.c:394
+#, c-format
+msgid "Expected None, \"OK\", \"SKIP\", or \"MODIFY\"."
+msgstr "Förväntade None, \"OK\", \"SKIP\" eller \"MODIFY\"."
+
+#: plpy_exec.c:444
+#, c-format
+msgid "PyList_SetItem() failed, while setting up arguments"
+msgstr "PyList_SetItem() misslyckades vid uppsättning av argument"
+
+#: plpy_exec.c:448
+#, c-format
+msgid "PyDict_SetItemString() failed, while setting up arguments"
+msgstr "PyDict_SetItemString() misslyckades vid uppsättning av argument"
+
+#: plpy_exec.c:460
+#, c-format
+msgid "function returning record called in context that cannot accept type record"
+msgstr "en funktion med post som värde anropades i sammanhang där poster inte kan godtagas."
+
+#: plpy_exec.c:677
+#, c-format
+msgid "while creating return value"
+msgstr "vid skapande av returvärde"
+
+#: plpy_exec.c:924
+#, c-format
+msgid "TD[\"new\"] deleted, cannot modify row"
+msgstr "TD[\"new\"] raderad, kan inte modifiera rad"
+
+#: plpy_exec.c:929
+#, c-format
+msgid "TD[\"new\"] is not a dictionary"
+msgstr "TD[\"new\"] är inte en dictionary"
+
+#: plpy_exec.c:954
+#, c-format
+msgid "TD[\"new\"] dictionary key at ordinal position %d is not a string"
+msgstr "TD[\"new\"] dictionary-nyckel vid numerisk position %d är inte en sträng"
+
+#: plpy_exec.c:961
+#, c-format
+msgid "key \"%s\" found in TD[\"new\"] does not exist as a column in the triggering row"
+msgstr "nyckel \"%s\" hittad i TD[\"new\"] finns inte som en kolumn i den triggande raden"
+
+#: plpy_exec.c:966
+#, c-format
+msgid "cannot set system attribute \"%s\""
+msgstr "kan inte sätta systemattribut \"%s\""
+
+#: plpy_exec.c:971
+#, c-format
+msgid "cannot set generated column \"%s\""
+msgstr "kan inte sätta genererad kolumn \"%s\""
+
+#: plpy_exec.c:1029
+#, c-format
+msgid "while modifying trigger row"
+msgstr "vid modifiering av triggerrad"
+
+#: plpy_exec.c:1087
+#, c-format
+msgid "forcibly aborting a subtransaction that has not been exited"
+msgstr "tvingar avbrytande av subtransaktion som inte har avslutats"
+
+#: plpy_main.c:109
+#, c-format
+msgid "multiple Python libraries are present in session"
+msgstr "multipla Python-bibliotek är aktiva i sessionen"
+
+#: plpy_main.c:110
+#, c-format
+msgid "Only one Python major version can be used in one session."
+msgstr "Bara en major-version av Python kan användas i en session."
+
+#: plpy_main.c:122
+#, c-format
+msgid "untrapped error in initialization"
+msgstr "ej fångar fel i initiering"
+
+#: plpy_main.c:145
+#, c-format
+msgid "could not import \"__main__\" module"
+msgstr "kunde inte importera \"__main__\"-modul"
+
+#: plpy_main.c:154
+#, c-format
+msgid "could not initialize globals"
+msgstr "kunde inte initierar globaler"
+
+#: plpy_main.c:352
+#, c-format
+msgid "PL/Python procedure \"%s\""
+msgstr "PL/Python-procedur \"%s\""
+
+#: plpy_main.c:355
+#, c-format
+msgid "PL/Python function \"%s\""
+msgstr "PL/Python-funktion \"%s\""
+
+#: plpy_main.c:363
+#, c-format
+msgid "PL/Python anonymous code block"
+msgstr "PL/Python anonymt kodblock"
+
+#: plpy_plpymodule.c:168 plpy_plpymodule.c:171
+#, c-format
+msgid "could not import \"plpy\" module"
+msgstr "kunde inte importera \"plpy\"-modul"
+
+#: plpy_plpymodule.c:182
+#, c-format
+msgid "could not create the spiexceptions module"
+msgstr "kunde inte skapa modulen spiexceptions"
+
+#: plpy_plpymodule.c:190
+#, c-format
+msgid "could not add the spiexceptions module"
+msgstr "kunde inte lägga till modulen spiexceptions"
+
+#: plpy_plpymodule.c:257
+#, c-format
+msgid "could not generate SPI exceptions"
+msgstr "kunde inte skapa SPI-undantag"
+
+#: plpy_plpymodule.c:425
+#, c-format
+msgid "could not unpack arguments in plpy.elog"
+msgstr "kunde inte packa upp argument i plpy.elog"
+
+#: plpy_plpymodule.c:434
+msgid "could not parse error message in plpy.elog"
+msgstr "kunde inte parsa felmeddelande i plpy.elog"
+
+#: plpy_plpymodule.c:451
+#, c-format
+msgid "argument 'message' given by name and position"
+msgstr "argumentet 'message' angivet med namn och position"
+
+#: plpy_plpymodule.c:478
+#, c-format
+msgid "'%s' is an invalid keyword argument for this function"
+msgstr "'%s' är ett ogiltigt nyckelordsargument för denna funktion"
+
+#: plpy_plpymodule.c:489 plpy_plpymodule.c:495
+#, c-format
+msgid "invalid SQLSTATE code"
+msgstr "ogiltig SQLSTATE-kod"
+
+#: plpy_procedure.c:225
+#, c-format
+msgid "trigger functions can only be called as triggers"
+msgstr "Triggningsfunktioner kan bara anropas vid triggning."
+
+#: plpy_procedure.c:229
+#, c-format
+msgid "PL/Python functions cannot return type %s"
+msgstr "PL/Python-funktioner kan inte returnera typ %s"
+
+#: plpy_procedure.c:307
+#, c-format
+msgid "PL/Python functions cannot accept type %s"
+msgstr "PL/Python-funktioner kan inte ta emot typ %s"
+
+#: plpy_procedure.c:395
+#, c-format
+msgid "could not compile PL/Python function \"%s\""
+msgstr "kunde inte kompilera PL/Python-funktion \"%s\""
+
+#: plpy_procedure.c:398
+#, c-format
+msgid "could not compile anonymous PL/Python code block"
+msgstr "kunde inte kompilera anonymt PL/Python-kodblock"
+
+#: plpy_resultobject.c:117 plpy_resultobject.c:143 plpy_resultobject.c:169
+#, c-format
+msgid "command did not produce a result set"
+msgstr "kommandot producerade inte en resultatmängd"
+
+#: plpy_spi.c:56
+#, c-format
+msgid "second argument of plpy.prepare must be a sequence"
+msgstr "andra argumentet till plpy.prepare måste vara en sekvens"
+
+#: plpy_spi.c:98
+#, c-format
+msgid "plpy.prepare: type name at ordinal position %d is not a string"
+msgstr "plpy.prepare: typnamn vid numerisk position %d är inte en sträng"
+
+#: plpy_spi.c:170
+#, c-format
+msgid "plpy.execute expected a query or a plan"
+msgstr "plpy.execute förväntade en fråga eller en plan"
+
+#: plpy_spi.c:189
+#, c-format
+msgid "plpy.execute takes a sequence as its second argument"
+msgstr "plpy.execute tar en sekvens som sitt andra argument"
+
+#: plpy_spi.c:297
+#, c-format
+msgid "SPI_execute_plan failed: %s"
+msgstr "SPI_execute_plan misslyckades: %s"
+
+#: plpy_spi.c:339
+#, c-format
+msgid "SPI_execute failed: %s"
+msgstr "SPI_execute misslyckades: %s"
+
+#: plpy_subxactobject.c:92
+#, c-format
+msgid "this subtransaction has already been entered"
+msgstr "denna subtransaktion har redan gåtts in i"
+
+#: plpy_subxactobject.c:98 plpy_subxactobject.c:156
+#, c-format
+msgid "this subtransaction has already been exited"
+msgstr "denna subtransaktion har redan avslutat"
+
+#: plpy_subxactobject.c:150
+#, c-format
+msgid "this subtransaction has not been entered"
+msgstr "denna subtransaktion har inte gåtts in i"
+
+#: plpy_subxactobject.c:162
+#, c-format
+msgid "there is no subtransaction to exit from"
+msgstr "det finns ingen subtransaktion att avsluta från"
+
+#: plpy_typeio.c:588
+#, c-format
+msgid "could not import a module for Decimal constructor"
+msgstr "kunde inte importera en modul för Decimal-konstruktorn"
+
+#: plpy_typeio.c:592
+#, c-format
+msgid "no Decimal attribute in module"
+msgstr "inga Decimal-attribut i modulen"
+
+#: plpy_typeio.c:598
+#, c-format
+msgid "conversion from numeric to Decimal failed"
+msgstr "konvertering från numeric till Decimal misslyckades"
+
+#: plpy_typeio.c:912
+#, c-format
+msgid "could not create bytes representation of Python object"
+msgstr "kunde inte skapa byte-representation av Python-objekt"
+
+#: plpy_typeio.c:1049
+#, c-format
+msgid "could not create string representation of Python object"
+msgstr "kunde inte skapa strängrepresentation av Python-objekt"
+
+#: plpy_typeio.c:1060
+#, c-format
+msgid "could not convert Python object into cstring: Python string representation appears to contain null bytes"
+msgstr "kunde inte konvertera Python-objekt till cstring: Python-strängrepresentationen verkar innehålla noll-bytes"
+
+#: plpy_typeio.c:1157
+#, c-format
+msgid "return value of function with array return type is not a Python sequence"
+msgstr "returvärde för funktion med array-returtyp är inte en Python-sekvens"
+
+#: plpy_typeio.c:1202
+#, c-format
+msgid "could not determine sequence length for function return value"
+msgstr "kunde inte bestämma sekvenslängd för funktionens returvärde"
+
+#: plpy_typeio.c:1222 plpy_typeio.c:1237 plpy_typeio.c:1253
+#, c-format
+msgid "multidimensional arrays must have array expressions with matching dimensions"
+msgstr "flerdimensionella vektorer måste ha array-uttryck av passande dimensioner"
+
+#: plpy_typeio.c:1227
+#, c-format
+msgid "number of array dimensions exceeds the maximum allowed (%d)"
+msgstr "antal array-dimensioner överskriver maximalt tillåtna (%d)"
+
+#: plpy_typeio.c:1329
+#, c-format
+msgid "malformed record literal: \"%s\""
+msgstr "felaktig postliteral: \"%s\""
+
+#: plpy_typeio.c:1330
+#, c-format
+msgid "Missing left parenthesis."
+msgstr "Saknar vänster parentes"
+
+#: plpy_typeio.c:1331 plpy_typeio.c:1532
+#, c-format
+msgid "To return a composite type in an array, return the composite type as a Python tuple, e.g., \"[('foo',)]\"."
+msgstr "För att returnera en composite-typ i en array, returnera composite-typen som en Python-tupel, t.ex. \"[('foo',)]\"."
+
+#: plpy_typeio.c:1378
+#, c-format
+msgid "key \"%s\" not found in mapping"
+msgstr "nyckeln \"%s\" hittades inte i mapping"
+
+#: plpy_typeio.c:1379
+#, c-format
+msgid "To return null in a column, add the value None to the mapping with the key named after the column."
+msgstr "För att returnera null i en kolumn så lägg till värdet None till mappningen med nyckelnamn taget från kolumnen."
+
+#: plpy_typeio.c:1432
+#, c-format
+msgid "length of returned sequence did not match number of columns in row"
+msgstr "längden på den returnerade sekvensen matchade inte antal kolumner i raden"
+
+#: plpy_typeio.c:1530
+#, c-format
+msgid "attribute \"%s\" does not exist in Python object"
+msgstr "attributet \"%s\" finns inte i Python-objektet"
+
+#: plpy_typeio.c:1533
+#, c-format
+msgid "To return null in a column, let the returned object have an attribute named after column with value None."
+msgstr "För att returnera null i en kolumn så låt det returnerade objektet ha ett attribut med namn efter kolumnen och med värdet None."
+
+#: plpy_util.c:31
+#, c-format
+msgid "could not convert Python Unicode object to bytes"
+msgstr "kunde inte konvertera Python-unicode-objekt till bytes"
+
+#: plpy_util.c:37
+#, c-format
+msgid "could not extract bytes from encoded string"
+msgstr "kunde inte extrahera bytes från kodad sträng"
+
+#, c-format
+#~ msgid "To construct a multidimensional array, the inner sequences must all have the same length."
+#~ msgstr "För att skapa en multidimensionell array så skall alla de inre sekvenserna ha samma längd."
+
+#, c-format
+#~ msgid "array size exceeds the maximum allowed"
+#~ msgstr "array-storlek överskrider maximalt tillåtna"
+
+#, c-format
+#~ msgid "wrong length of inner sequence: has length %d, but %d was expected"
+#~ msgstr "fel längd på inre sekvens: har längd %d, men %d förväntades"
diff --git a/src/pl/plpython/po/tr.po b/src/pl/plpython/po/tr.po
new file mode 100644
index 0000000..330162a
--- /dev/null
+++ b/src/pl/plpython/po/tr.po
@@ -0,0 +1,537 @@
+# LANGUAGE message translation file for plpython
+# Copyright (C) 2009 PostgreSQL Global Development Group
+# This file is distributed under the same license as the PostgreSQL package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, 2009.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: PostgreSQL 8.4\n"
+"Report-Msgid-Bugs-To: pgsql-bugs@lists.postgresql.org\n"
+"POT-Creation-Date: 2019-04-26 13:38+0000\n"
+"PO-Revision-Date: 2019-06-13 17:10+0300\n"
+"Last-Translator: Abdullah G. Gülner <agulner@gmail.com>\n"
+"Language-Team: Turkish <devrim@gunduz.org>\n"
+"Language: tr\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=1; plural=0;\n"
+"X-Generator: Poedit 1.8.7.1\n"
+
+#: plpy_cursorobject.c:78
+#, c-format
+msgid "plpy.cursor expected a query or a plan"
+msgstr "plpy.cursor bir sorgu ya da bir plan bekledi"
+
+#: plpy_cursorobject.c:161
+#, c-format
+msgid "plpy.cursor takes a sequence as its second argument"
+msgstr "plpy.cursor bir sequence'ı ikinci argüman olarak alır"
+
+#: plpy_cursorobject.c:177 plpy_spi.c:211
+#, c-format
+msgid "could not execute plan"
+msgstr "plan çalıştırılamadı"
+
+#: plpy_cursorobject.c:180 plpy_spi.c:214
+#, c-format
+msgid "Expected sequence of %d argument, got %d: %s"
+msgid_plural "Expected sequence of %d arguments, got %d: %s"
+msgstr[0] "%d argümanının sequence'ı beklendi; %d alındı: %s"
+
+#: plpy_cursorobject.c:329
+#, c-format
+msgid "iterating a closed cursor"
+msgstr "kapalı bir imleç (cursor) yineleniyor"
+
+#: plpy_cursorobject.c:337 plpy_cursorobject.c:403
+#, c-format
+msgid "iterating a cursor in an aborted subtransaction"
+msgstr "iptal edilen bir alt-işlemdeki (subtransaction) bir cursor yineleniyor"
+
+#: plpy_cursorobject.c:395
+#, c-format
+msgid "fetch from a closed cursor"
+msgstr "kapalı bir cursor'dan getir"
+
+#: plpy_cursorobject.c:438 plpy_spi.c:409
+#, c-format
+msgid "query result has too many rows to fit in a Python list"
+msgstr "sorgu sonucundaki satır sayısı bir Python listesine sığabilecekten çok fazla "
+
+#: plpy_cursorobject.c:490
+#, c-format
+msgid "closing a cursor in an aborted subtransaction"
+msgstr "iptal edilen bir alt-işlemdeki (subtransaction) bir cursor kapatılıyor"
+
+#: plpy_elog.c:129 plpy_elog.c:130 plpy_plpymodule.c:553
+#, c-format
+msgid "%s"
+msgstr "%s"
+
+#: plpy_exec.c:143
+#, c-format
+msgid "unsupported set function return mode"
+msgstr "desteklenmeyen küme fonksiyonu dönüş modu"
+
+#: plpy_exec.c:144
+#, c-format
+msgid "PL/Python set-returning functions only support returning one value per call."
+msgstr "PL/Python küme dönen fonksiyonları sadece her çağrı içinde bir değer döndürmeyi desteklerler"
+
+#: plpy_exec.c:157
+#, c-format
+msgid "returned object cannot be iterated"
+msgstr "dönen nesne yinelenemez"
+
+#: plpy_exec.c:158
+#, c-format
+msgid "PL/Python set-returning functions must return an iterable object."
+msgstr "PL/Python küme dönen fonksiyonları yinelenebilir bir nesne dönmelidir."
+
+#: plpy_exec.c:172
+#, c-format
+msgid "error fetching next item from iterator"
+msgstr "yineleticiden sonraki öğeyi alırken hata"
+
+#: plpy_exec.c:215
+#, c-format
+msgid "PL/Python procedure did not return None"
+msgstr "PL/Python prosedürü None döndürmedi"
+
+#: plpy_exec.c:219
+#, c-format
+msgid "PL/Python function with return type \"void\" did not return None"
+msgstr "dönüş tipi \"void\" olan PL/Python fonksiyonu None döndürmedi"
+
+#: plpy_exec.c:375 plpy_exec.c:401
+#, c-format
+msgid "unexpected return value from trigger procedure"
+msgstr "trigger yordamından beklenmeyen dönüş değeri"
+
+#: plpy_exec.c:376
+#, c-format
+msgid "Expected None or a string."
+msgstr "None ya da string bekleniyordu."
+
+#: plpy_exec.c:391
+#, c-format
+msgid "PL/Python trigger function returned \"MODIFY\" in a DELETE trigger -- ignored"
+msgstr "PL/Python trigger fonksiyonu DELETE triggerında \"MODIFY\" döndürdü -- gözardı edildi"
+
+#: plpy_exec.c:402
+#, c-format
+msgid "Expected None, \"OK\", \"SKIP\", or \"MODIFY\"."
+msgstr "None, \"OK\", \"SKIP\", ya da \"MODIFY\" bekleniyordu"
+
+#: plpy_exec.c:452
+#, c-format
+msgid "PyList_SetItem() failed, while setting up arguments"
+msgstr "PyList_SetItem() bağımsız değişkenler ayarlanırken başarısız oldu"
+
+#: plpy_exec.c:456
+#, c-format
+msgid "PyDict_SetItemString() failed, while setting up arguments"
+msgstr "PyDict_SetItemString() bağımsız değişkenler ayarlanırken başarısız oldu"
+
+#: plpy_exec.c:468
+#, c-format
+msgid "function returning record called in context that cannot accept type record"
+msgstr "tip kaydı içermeyen alanda çağırılan ve kayıt döndüren fonksiyon"
+
+#: plpy_exec.c:685
+#, c-format
+msgid "while creating return value"
+msgstr "dönüş değeri yaratılırken"
+
+#: plpy_exec.c:919
+#, c-format
+msgid "TD[\"new\"] deleted, cannot modify row"
+msgstr "TD[\"new\"] silindi, satır düzenlenemiyor"
+
+#: plpy_exec.c:924
+#, c-format
+msgid "TD[\"new\"] is not a dictionary"
+msgstr "TD[\"new\"] bir sözlük değil"
+
+#: plpy_exec.c:951
+#, c-format
+msgid "TD[\"new\"] dictionary key at ordinal position %d is not a string"
+msgstr "%d sıra pozisyonundaki TD[\"new\"] sözlük anahtarı dizi değil"
+
+#: plpy_exec.c:958
+#, c-format
+msgid "key \"%s\" found in TD[\"new\"] does not exist as a column in the triggering row"
+msgstr "TD[\"new\"] içinde bulunan \"%s\" anahtarı tetikleyen satırda bir kolon olarak bulunmuyor"
+
+#: plpy_exec.c:963
+#, c-format
+msgid "cannot set system attribute \"%s\""
+msgstr "\"%s\" sistem niteliği ayarlanamıyor"
+
+#: plpy_exec.c:968
+#, c-format
+msgid "cannot set generated column \"%s\""
+msgstr "oluşturulan \"%s\" sütunu ayarlanamıyor"
+
+#: plpy_exec.c:1026
+#, c-format
+msgid "while modifying trigger row"
+msgstr "tetikleyici satırını düzenlerken"
+
+#: plpy_exec.c:1087
+#, c-format
+msgid "forcibly aborting a subtransaction that has not been exited"
+msgstr "çıkış yapılmamış bir alt-işlem (subtransaction) zorla iptal ediliyor"
+
+#: plpy_main.c:125
+#, c-format
+msgid "multiple Python libraries are present in session"
+msgstr "oturumda birden çok Python kütüphanesi mevcut"
+
+#: plpy_main.c:126
+#, c-format
+msgid "Only one Python major version can be used in one session."
+msgstr "Bir oturumda sadece bir Python ana sürümü kullanılabilir."
+
+#: plpy_main.c:142
+#, c-format
+msgid "untrapped error in initialization"
+msgstr "ilklendirme aşamasında yakalanamayan hata"
+
+#: plpy_main.c:165
+#, c-format
+msgid "could not import \"__main__\" module"
+msgstr "\"__main__\" modülü alınamadı"
+
+#: plpy_main.c:174
+#, c-format
+msgid "could not initialize globals"
+msgstr "global değerler ilklendirilemedi"
+
+#: plpy_main.c:399
+#, c-format
+msgid "PL/Python procedure \"%s\""
+msgstr "\"%s\" PL/Python prosedürü"
+
+#: plpy_main.c:402
+#, c-format
+msgid "PL/Python function \"%s\""
+msgstr "\"%s\" PL/Python fonksiyonu"
+
+#: plpy_main.c:410
+#, c-format
+msgid "PL/Python anonymous code block"
+msgstr "PL/Python anonim kod bloğu"
+
+#: plpy_plpymodule.c:186 plpy_plpymodule.c:189
+#, c-format
+msgid "could not import \"plpy\" module"
+msgstr "\"plpy\" modülü alınamadı"
+
+#: plpy_plpymodule.c:204
+#, c-format
+msgid "could not create the spiexceptions module"
+msgstr "spiexceptions modülü oluşturulamadı"
+
+#: plpy_plpymodule.c:212
+#, c-format
+msgid "could not add the spiexceptions module"
+msgstr "spiexceptions modülü eklenemedi"
+
+#: plpy_plpymodule.c:280
+#, c-format
+msgid "could not generate SPI exceptions"
+msgstr "SPI istisnaları (exception) üretilemedi"
+
+#: plpy_plpymodule.c:448
+#, c-format
+msgid "could not unpack arguments in plpy.elog"
+msgstr "plpy.elog dosyasındaki argümanlar unpack edilemedi"
+
+#: plpy_plpymodule.c:457
+msgid "could not parse error message in plpy.elog"
+msgstr "plpy.elog dosyasındaki hata mesajı ayrıştırılamadı"
+
+#: plpy_plpymodule.c:474
+#, c-format
+msgid "argument 'message' given by name and position"
+msgstr "ad ve konum tarafından verilen argüman 'mesajı'"
+
+#: plpy_plpymodule.c:501
+#, c-format
+msgid "'%s' is an invalid keyword argument for this function"
+msgstr "'%s' bu fonksiyon için geçersiz bir anahtar kelime argümanıdır"
+
+#: plpy_plpymodule.c:512 plpy_plpymodule.c:518
+#, c-format
+msgid "invalid SQLSTATE code"
+msgstr "geçersiz SQLSTATE kodu"
+
+#: plpy_procedure.c:230
+#, c-format
+msgid "trigger functions can only be called as triggers"
+msgstr "trigger fonksiyonları sadece trigger olarak çağırılabilirler."
+
+#: plpy_procedure.c:234
+#, c-format
+msgid "PL/Python functions cannot return type %s"
+msgstr "PL/Python fonksiyonları %s tipini döndüremezler"
+
+#: plpy_procedure.c:312
+#, c-format
+msgid "PL/Python functions cannot accept type %s"
+msgstr "PL/Python fonksiyonlar %s tipini kabul etmezler"
+
+#: plpy_procedure.c:402
+#, c-format
+msgid "could not compile PL/Python function \"%s\""
+msgstr "\"%s\" PL/Python fonksiyonu derlenemedi"
+
+#: plpy_procedure.c:405
+#, c-format
+msgid "could not compile anonymous PL/Python code block"
+msgstr "anonim PL/Python kod bloğu derlenemedi"
+
+#: plpy_resultobject.c:121 plpy_resultobject.c:147 plpy_resultobject.c:173
+#, c-format
+msgid "command did not produce a result set"
+msgstr "komut bir sonuç kümesi üretmedi"
+
+#: plpy_spi.c:60
+#, c-format
+msgid "second argument of plpy.prepare must be a sequence"
+msgstr "plpy.prepare'in ikinci argümanı sequence olmalıdır"
+
+#: plpy_spi.c:104
+#, c-format
+msgid "plpy.prepare: type name at ordinal position %d is not a string"
+msgstr "plpy.prepare: %d sıra posizyonundaki veri tipi dizi değil"
+
+#: plpy_spi.c:176
+#, c-format
+msgid "plpy.execute expected a query or a plan"
+msgstr "plpy.execute bir sorgu ya da bir plan bekledi"
+
+#: plpy_spi.c:195
+#, c-format
+msgid "plpy.execute takes a sequence as its second argument"
+msgstr "plpy.execute bir sequence'ı ikinci argüman olarak alır"
+
+#: plpy_spi.c:305
+#, c-format
+msgid "SPI_execute_plan failed: %s"
+msgstr "SPI_execute_plan başarısız oldu: %s"
+
+#: plpy_spi.c:347
+#, c-format
+msgid "SPI_execute failed: %s"
+msgstr "SPI_execute başarısız oldu: %s"
+
+#: plpy_subxactobject.c:97
+#, c-format
+msgid "this subtransaction has already been entered"
+msgstr "bu alt-işleme (subtransaction) zaten girilmiş"
+
+#: plpy_subxactobject.c:103 plpy_subxactobject.c:161
+#, c-format
+msgid "this subtransaction has already been exited"
+msgstr "bu alt-işlemden (subtransaction) zaten çıkılmış"
+
+#: plpy_subxactobject.c:155
+#, c-format
+msgid "this subtransaction has not been entered"
+msgstr "bu alt-işleme (subtransaction) girilmemiş"
+
+#: plpy_subxactobject.c:167
+#, c-format
+msgid "there is no subtransaction to exit from"
+msgstr "çıkılacak bir alt-işlem (subtransaction) yok"
+
+#: plpy_typeio.c:591
+#, c-format
+msgid "could not import a module for Decimal constructor"
+msgstr "Decimal constructor için bir modül alınamadı"
+
+#: plpy_typeio.c:595
+#, c-format
+msgid "no Decimal attribute in module"
+msgstr "modülde Decimal niteliği yok"
+
+#: plpy_typeio.c:601
+#, c-format
+msgid "conversion from numeric to Decimal failed"
+msgstr "numeric'ten Decimal'e dönüşüm başarısız oldu"
+
+#: plpy_typeio.c:915
+#, c-format
+msgid "could not create bytes representation of Python object"
+msgstr "Python nesnesinin bytes gösterimi yaratılamadı"
+
+#: plpy_typeio.c:1063
+#, c-format
+msgid "could not create string representation of Python object"
+msgstr "Python nesnesinin dizgi gösterimi yaratılamadı"
+
+#: plpy_typeio.c:1074
+#, c-format
+msgid "could not convert Python object into cstring: Python string representation appears to contain null bytes"
+msgstr "Python nesnesi cstring'e dönüştürülemedi: Python dizgi gösterimi null bayt içeriyor olabilir."
+
+#: plpy_typeio.c:1183
+#, c-format
+msgid "number of array dimensions exceeds the maximum allowed (%d)"
+msgstr "dizi (array) boyut sayısı izin verilen en yüksek değeri (%d) aşmaktadır"
+
+#: plpy_typeio.c:1187
+#, c-format
+msgid "could not determine sequence length for function return value"
+msgstr "fonksiyon dönüş değeri için sequence uzunluğu belirlenemedi"
+
+#: plpy_typeio.c:1190 plpy_typeio.c:1194
+#, c-format
+msgid "array size exceeds the maximum allowed"
+msgstr "dizi (array) boyutu izin verilen en yüksek değeri aşmaktadır"
+
+#: plpy_typeio.c:1220
+#, c-format
+msgid "return value of function with array return type is not a Python sequence"
+msgstr "dizi dönüp tipli dönüş değeri olan fonksiyon Python sequence'ı değildir"
+
+#: plpy_typeio.c:1266
+#, c-format
+msgid "wrong length of inner sequence: has length %d, but %d was expected"
+msgstr "iç sequence'in uzunluğu yanlış: %d uzunlukta, fakat %d bekleniyordu"
+
+#: plpy_typeio.c:1268
+#, c-format
+msgid "To construct a multidimensional array, the inner sequences must all have the same length."
+msgstr "Çok boyutlu bir dizi oluşturmak için, iç sequence'lerin tamamı aynı uzunlukta olmalı."
+
+#: plpy_typeio.c:1347
+#, c-format
+msgid "malformed record literal: \"%s\""
+msgstr "hatalı değer: \"%s\""
+
+#: plpy_typeio.c:1348
+#, c-format
+msgid "Missing left parenthesis."
+msgstr "Sol parantez eksik."
+
+#: plpy_typeio.c:1349 plpy_typeio.c:1550
+#, c-format
+msgid "To return a composite type in an array, return the composite type as a Python tuple, e.g., \"[('foo',)]\"."
+msgstr "Bir bileşik türü dizi (array) içinde döndürmek için, bileşik türü bir Python tuple, e.g., \"[('foo',)]\"."
+
+#: plpy_typeio.c:1396
+#, c-format
+msgid "key \"%s\" not found in mapping"
+msgstr "\"%s\" anahtarı planlamada bulunnamadı"
+
+#: plpy_typeio.c:1397
+#, c-format
+msgid "To return null in a column, add the value None to the mapping with the key named after the column."
+msgstr "Bir kolondan Null döndürmek için, kolonun ismindeki eşleşmenin anahtarına, NONE değerini ekleyin"
+
+#: plpy_typeio.c:1450
+#, c-format
+msgid "length of returned sequence did not match number of columns in row"
+msgstr "Dönen sequence'in uzunluğu satırdaki kolonların sayısı ile eşleşmiyor."
+
+#: plpy_typeio.c:1548
+#, c-format
+msgid "attribute \"%s\" does not exist in Python object"
+msgstr "\"%s\" niteliği Python nesnesinde bulunmaz"
+
+#: plpy_typeio.c:1551
+#, c-format
+msgid "To return null in a column, let the returned object have an attribute named after column with value None."
+msgstr " Bir kolondan null döndürmek için, döndürdüğünüz nesnenin, kolonun adına sahip bir özelliğinin olmasını ve bu özelliğin değerinin NONE olmasını sağlamanız gerekir"
+
+#: plpy_util.c:35
+#, c-format
+msgid "could not convert Python Unicode object to bytes"
+msgstr "Python unicode nesnesi baytlara dönüştürülemedi."
+
+#: plpy_util.c:41
+#, c-format
+msgid "could not extract bytes from encoded string"
+msgstr "kodlanmış string den baytlar çıkarılamadı"
+
+#~ msgid "could not create new dictionary while building trigger arguments"
+#~ msgstr "trigger argümanlarını oluştururken yeni sözlük yaratılamadı"
+
+#~ msgid "could not create globals"
+#~ msgstr "evrensel değerler (globals) oluşturulamadı"
+
+#~ msgid "could not create exception \"%s\""
+#~ msgstr "\"%s\" istisnası (exception) oluşturulamadı"
+
+#~ msgid "could not create new dictionary"
+#~ msgstr "Yeni sözlük yaratılamadı"
+
+#~ msgid "PL/Python function \"%s\" could not execute plan"
+#~ msgstr "\"%s\" PL/Python fonksiyonu planı çalıştıramadı"
+
+#~ msgid "PL/Python function \"%s\" failed"
+#~ msgstr "\"%s\" PL/Python fonksiyonu başarısız oldu"
+
+#~ msgid "could not create string representation of Python object in PL/Python function \"%s\" while creating return value"
+#~ msgstr "dönüş değeri yaratılırken \"%s\" Pl/Python fonksiyonunun içindeki Python ensnesinin dizi gösterimi yaratılamadı"
+
+#~ msgid "could not compute string representation of Python object in PL/Python function \"%s\" while modifying trigger row"
+#~ msgstr "tetikleyici satırı düzenlerken \"%s\" PL/Python fonksiyonunun içindeki Python nesnesinin dizi gösterimi hesaplanamadı"
+
+#~ msgid "out of memory"
+#~ msgstr "yetersiz bellek"
+
+#~ msgid "PL/Python: %s"
+#~ msgstr "PL/Python: %s"
+
+#~ msgid "could not create procedure cache"
+#~ msgstr "yordam önbelleği yaratılamadı"
+
+#~ msgid "Start a new session to use a different Python major version."
+#~ msgstr "Farklı bir Python ana sürümü kullanmak için yeni bir oturum açın."
+
+#~ msgid "This session has previously used Python major version %d, and it is now attempting to use Python major version %d."
+#~ msgstr "Bu oturum daha önceden %d Python ana sürümünü kullandı, ve şimdi %d ana sürümünü kullanmayı deniyor."
+
+#~ msgid "unrecognized error in PLy_spi_execute_fetch_result"
+#~ msgstr "PLy_spi_execute_fetch_result içinde tanımlanamayan hata"
+
+#~ msgid "unrecognized error in PLy_spi_execute_query"
+#~ msgstr "PLy_spi_execute_query içinde tanımlanamayan hata"
+
+#~ msgid "unrecognized error in PLy_spi_execute_plan"
+#~ msgstr "PLy_spi_execute_plan içinde beklenmeyen hata"
+
+#~ msgid "unrecognized error in PLy_spi_prepare"
+#~ msgstr "PLy_spi_prepare içinde tanımlanamayan hata"
+
+#~ msgid "plpy.prepare does not support composite types"
+#~ msgstr "plpy.prepare kompozit tipleri desteklemez"
+
+#~ msgid "invalid arguments for plpy.prepare"
+#~ msgstr "plpy.prepare için geçersiz argümanlar"
+
+#~ msgid "transaction aborted"
+#~ msgstr "transaction iptal edildi"
+
+#~ msgid "plan.status takes no arguments"
+#~ msgstr "plan.status bir argüman almaz"
+
+#~ msgid "PL/Python only supports one-dimensional arrays."
+#~ msgstr "PL/Python sadece bir boyutlu dizileri destekler."
+
+#~ msgid "cannot convert multidimensional array to Python list"
+#~ msgstr "çok boyutlu dizi, Python listesine dönüştürülemedi"
+
+#~ msgid "PL/Python does not support conversion to arrays of row types."
+#~ msgstr "PL/Python satır tiplerinin dizilere dönüşümünü desteklemez."
+
+#~ msgid "PyCObject_FromVoidPtr() failed"
+#~ msgstr "PyCObject_FromVoidPtr() başarısız oldu"
+
+#~ msgid "PyCObject_AsVoidPtr() failed"
+#~ msgstr "PyCObject_AsVoidPtr() başarısız oldu"
diff --git a/src/pl/plpython/po/uk.po b/src/pl/plpython/po/uk.po
new file mode 100644
index 0000000..0388016
--- /dev/null
+++ b/src/pl/plpython/po/uk.po
@@ -0,0 +1,452 @@
+msgid ""
+msgstr ""
+"Project-Id-Version: postgresql\n"
+"Report-Msgid-Bugs-To: pgsql-bugs@lists.postgresql.org\n"
+"POT-Creation-Date: 2023-12-17 22:08+0000\n"
+"PO-Revision-Date: 2023-12-20 11:53\n"
+"Last-Translator: \n"
+"Language-Team: Ukrainian\n"
+"Language: uk_UA\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=4; plural=((n%10==1 && n%100!=11) ? 0 : ((n%10 >= 2 && n%10 <=4 && (n%100 < 12 || n%100 > 14)) ? 1 : ((n%10 == 0 || (n%10 >= 5 && n%10 <=9)) || (n%100 >= 11 && n%100 <= 14)) ? 2 : 3));\n"
+"X-Crowdin-Project: postgresql\n"
+"X-Crowdin-Project-ID: 324573\n"
+"X-Crowdin-Language: uk\n"
+"X-Crowdin-File: /REL_16_STABLE/plpython.pot\n"
+"X-Crowdin-File-ID: 933\n"
+
+#: plpy_cursorobject.c:72
+#, c-format
+msgid "plpy.cursor expected a query or a plan"
+msgstr "plpy.cursor очікував запит або план"
+
+#: plpy_cursorobject.c:155
+#, c-format
+msgid "plpy.cursor takes a sequence as its second argument"
+msgstr "plpy.сursor приймає як другий аргумент послідовність"
+
+#: plpy_cursorobject.c:171 plpy_spi.c:205
+#, c-format
+msgid "could not execute plan"
+msgstr "не вдалося виконати план"
+
+#: plpy_cursorobject.c:174 plpy_spi.c:208
+#, c-format
+msgid "Expected sequence of %d argument, got %d: %s"
+msgid_plural "Expected sequence of %d arguments, got %d: %s"
+msgstr[0] "Очікувалась послідовність %d аргументів, отримано %d: %s"
+msgstr[1] "Очікувалась послідовність %d аргументів, отримано %d: %s"
+msgstr[2] "Очікувалась послідовність %d аргументів, отримано %d: %s"
+msgstr[3] "Очікувалась послідовність %d аргумента, отримано %d: %s"
+
+#: plpy_cursorobject.c:321
+#, c-format
+msgid "iterating a closed cursor"
+msgstr "ітерація закритого курсора"
+
+#: plpy_cursorobject.c:329 plpy_cursorobject.c:395
+#, c-format
+msgid "iterating a cursor in an aborted subtransaction"
+msgstr "ітерація курсора в перерваній субтранзакції"
+
+#: plpy_cursorobject.c:387
+#, c-format
+msgid "fetch from a closed cursor"
+msgstr "витяг з закритого курсору"
+
+#: plpy_cursorobject.c:430 plpy_spi.c:401
+#, c-format
+msgid "query result has too many rows to fit in a Python list"
+msgstr "результат запиту має забагато рядків для передачі у список Python"
+
+#: plpy_cursorobject.c:482
+#, c-format
+msgid "closing a cursor in an aborted subtransaction"
+msgstr "закриття курсора в перерваній транзакції"
+
+#: plpy_elog.c:122 plpy_elog.c:123 plpy_plpymodule.c:530
+#, c-format
+msgid "%s"
+msgstr "%s"
+
+#: plpy_exec.c:139
+#, c-format
+msgid "unsupported set function return mode"
+msgstr "режим не підтримується для функцій, що повертають набір"
+
+#: plpy_exec.c:140
+#, c-format
+msgid "PL/Python set-returning functions only support returning one value per call."
+msgstr "функції PL/Python підтримують лише одне значення на виклик, коли повертають набір."
+
+#: plpy_exec.c:153
+#, c-format
+msgid "returned object cannot be iterated"
+msgstr "повернутий об'єкт не підтримує ітерації"
+
+#: plpy_exec.c:154
+#, c-format
+msgid "PL/Python set-returning functions must return an iterable object."
+msgstr "функції PL/Python повинні повертати об'єкт з підтримкою ітерації, коли повертають набір."
+
+#: plpy_exec.c:168
+#, c-format
+msgid "error fetching next item from iterator"
+msgstr "помилка отримання наступного елемента від ітератора"
+
+#: plpy_exec.c:211
+#, c-format
+msgid "PL/Python procedure did not return None"
+msgstr "процедура PL/Python не повернула None"
+
+#: plpy_exec.c:215
+#, c-format
+msgid "PL/Python function with return type \"void\" did not return None"
+msgstr "PL/Python функція з типом результату \"void\" не повернули None"
+
+#: plpy_exec.c:369 plpy_exec.c:393
+#, c-format
+msgid "unexpected return value from trigger procedure"
+msgstr "неочікуване значення процедури тригера"
+
+#: plpy_exec.c:370
+#, c-format
+msgid "Expected None or a string."
+msgstr "Очікувалось None або рядок."
+
+#: plpy_exec.c:383
+#, c-format
+msgid "PL/Python trigger function returned \"MODIFY\" in a DELETE trigger -- ignored"
+msgstr "Тригерна функція PL/Python повернула \"MODIFY\" в тригері DELETE -- проігноровано"
+
+#: plpy_exec.c:394
+#, c-format
+msgid "Expected None, \"OK\", \"SKIP\", or \"MODIFY\"."
+msgstr "Очікувалось None, \"OK\", \"SKIP\" або \"MODIFY\"."
+
+#: plpy_exec.c:444
+#, c-format
+msgid "PyList_SetItem() failed, while setting up arguments"
+msgstr "помилка PyList_SetItem() під час встановлення параметрів"
+
+#: plpy_exec.c:448
+#, c-format
+msgid "PyDict_SetItemString() failed, while setting up arguments"
+msgstr "помилка PyDict_SetItemString() під час встановлення параметрів"
+
+#: plpy_exec.c:460
+#, c-format
+msgid "function returning record called in context that cannot accept type record"
+msgstr "функція, що повертає набір, викликана у контексті, що не приймає тип запис"
+
+#: plpy_exec.c:677
+#, c-format
+msgid "while creating return value"
+msgstr "під час створення значення результату"
+
+#: plpy_exec.c:924
+#, c-format
+msgid "TD[\"new\"] deleted, cannot modify row"
+msgstr "TD[\"new\"] видалено, неможливо змінити рядок"
+
+#: plpy_exec.c:929
+#, c-format
+msgid "TD[\"new\"] is not a dictionary"
+msgstr "TD[\"new\"] не є словником"
+
+#: plpy_exec.c:954
+#, c-format
+msgid "TD[\"new\"] dictionary key at ordinal position %d is not a string"
+msgstr "ключ словника TD[\"new\"] на порядковий позиції %d не є рядком"
+
+#: plpy_exec.c:961
+#, c-format
+msgid "key \"%s\" found in TD[\"new\"] does not exist as a column in the triggering row"
+msgstr "ключ \"%s\" знайдений у TD[\"new\"] не існує як стовпець у рядку тригера"
+
+#: plpy_exec.c:966
+#, c-format
+msgid "cannot set system attribute \"%s\""
+msgstr "не вдалося встановити системний атрибут \"%s\""
+
+#: plpy_exec.c:971
+#, c-format
+msgid "cannot set generated column \"%s\""
+msgstr "неможливо оновити згенерований стовпець \"%s\""
+
+#: plpy_exec.c:1029
+#, c-format
+msgid "while modifying trigger row"
+msgstr "під час зміни рядка тригера"
+
+#: plpy_exec.c:1087
+#, c-format
+msgid "forcibly aborting a subtransaction that has not been exited"
+msgstr "примусове переривання субтранзакції, яка не вийшла"
+
+#: plpy_main.c:109
+#, c-format
+msgid "multiple Python libraries are present in session"
+msgstr "декілька бібліотек Python присутні у сесії"
+
+#: plpy_main.c:110
+#, c-format
+msgid "Only one Python major version can be used in one session."
+msgstr "За один сеанс може використовуватися лише одна основна версія Python."
+
+#: plpy_main.c:122
+#, c-format
+msgid "untrapped error in initialization"
+msgstr "неопрацьована помилка під час ініціалізації"
+
+#: plpy_main.c:145
+#, c-format
+msgid "could not import \"__main__\" module"
+msgstr "не вдалося імпортувати \"__main__\" модуль"
+
+#: plpy_main.c:154
+#, c-format
+msgid "could not initialize globals"
+msgstr "не вдалося ініціалізувати globals"
+
+#: plpy_main.c:352
+#, c-format
+msgid "PL/Python procedure \"%s\""
+msgstr "PL/Python процедура \"%s\""
+
+#: plpy_main.c:355
+#, c-format
+msgid "PL/Python function \"%s\""
+msgstr "PL/Python функція \"%s\""
+
+#: plpy_main.c:363
+#, c-format
+msgid "PL/Python anonymous code block"
+msgstr "анонімні коди блоку PL/Python"
+
+#: plpy_plpymodule.c:168 plpy_plpymodule.c:171
+#, c-format
+msgid "could not import \"plpy\" module"
+msgstr "не вдалося імпортувати \"plpy\" модуль"
+
+#: plpy_plpymodule.c:182
+#, c-format
+msgid "could not create the spiexceptions module"
+msgstr "не вдалося створити spiexceptions модуль"
+
+#: plpy_plpymodule.c:190
+#, c-format
+msgid "could not add the spiexceptions module"
+msgstr "не вдалося додати spiexceptions модуль"
+
+#: plpy_plpymodule.c:257
+#, c-format
+msgid "could not generate SPI exceptions"
+msgstr "не вдається створити винятки SPI"
+
+#: plpy_plpymodule.c:425
+#, c-format
+msgid "could not unpack arguments in plpy.elog"
+msgstr "не вдалося розпакувати аргументи в plpy.elog"
+
+#: plpy_plpymodule.c:434
+msgid "could not parse error message in plpy.elog"
+msgstr "не вдалося проаналізувати повідомлення про помилку в plpy.elog"
+
+#: plpy_plpymodule.c:451
+#, c-format
+msgid "argument 'message' given by name and position"
+msgstr "аргумент 'повідомлення' виданий за ім'ям та розташуванням"
+
+#: plpy_plpymodule.c:478
+#, c-format
+msgid "'%s' is an invalid keyword argument for this function"
+msgstr "'%s' є неприпустимим ключовим словом-аргументом для цієї функції"
+
+#: plpy_plpymodule.c:489 plpy_plpymodule.c:495
+#, c-format
+msgid "invalid SQLSTATE code"
+msgstr "неприпустимий код SQLSTATE"
+
+#: plpy_procedure.c:225
+#, c-format
+msgid "trigger functions can only be called as triggers"
+msgstr "тригер-функція може викликатися лише як тригер"
+
+#: plpy_procedure.c:229
+#, c-format
+msgid "PL/Python functions cannot return type %s"
+msgstr "PL/Python функції не можуть повернути тип %s"
+
+#: plpy_procedure.c:307
+#, c-format
+msgid "PL/Python functions cannot accept type %s"
+msgstr "PL/Python функції не можуть прийняти тип %s"
+
+#: plpy_procedure.c:395
+#, c-format
+msgid "could not compile PL/Python function \"%s\""
+msgstr "не вдалося скомпілювати функцію PL/Python \"%s\""
+
+#: plpy_procedure.c:398
+#, c-format
+msgid "could not compile anonymous PL/Python code block"
+msgstr "не вдалося скомпілювати анонімні коди блоку PL/Python"
+
+#: plpy_resultobject.c:117 plpy_resultobject.c:143 plpy_resultobject.c:169
+#, c-format
+msgid "command did not produce a result set"
+msgstr "команда не створила набір результатів"
+
+#: plpy_spi.c:56
+#, c-format
+msgid "second argument of plpy.prepare must be a sequence"
+msgstr "другий аргумент plpy.prepare має бути послідовністю"
+
+#: plpy_spi.c:98
+#, c-format
+msgid "plpy.prepare: type name at ordinal position %d is not a string"
+msgstr "plpy.prepare: ім'я на порядковий позиції %d не є рядком"
+
+#: plpy_spi.c:170
+#, c-format
+msgid "plpy.execute expected a query or a plan"
+msgstr "plpy.execute очікував запит або план"
+
+#: plpy_spi.c:189
+#, c-format
+msgid "plpy.execute takes a sequence as its second argument"
+msgstr "plpy.execute приймає як другий аргумент послідовність"
+
+#: plpy_spi.c:297
+#, c-format
+msgid "SPI_execute_plan failed: %s"
+msgstr "SPI_execute_plan не спрацював: %s"
+
+#: plpy_spi.c:339
+#, c-format
+msgid "SPI_execute failed: %s"
+msgstr "SPI_execute не спрацював: %s"
+
+#: plpy_subxactobject.c:92
+#, c-format
+msgid "this subtransaction has already been entered"
+msgstr "ця субтранзакція вже почалась"
+
+#: plpy_subxactobject.c:98 plpy_subxactobject.c:156
+#, c-format
+msgid "this subtransaction has already been exited"
+msgstr "ця субтранзакція вже вийшла"
+
+#: plpy_subxactobject.c:150
+#, c-format
+msgid "this subtransaction has not been entered"
+msgstr "ця субтранзакція ще не почалася"
+
+#: plpy_subxactobject.c:162
+#, c-format
+msgid "there is no subtransaction to exit from"
+msgstr "немає субтранзакції, щоб з неї вийти"
+
+#: plpy_typeio.c:588
+#, c-format
+msgid "could not import a module for Decimal constructor"
+msgstr "не вдалося імпортувати модуль для конструктора Decimal"
+
+#: plpy_typeio.c:592
+#, c-format
+msgid "no Decimal attribute in module"
+msgstr "відсутній атрибут Decimal у модулі"
+
+#: plpy_typeio.c:598
+#, c-format
+msgid "conversion from numeric to Decimal failed"
+msgstr "не вдалося виконати перетворення з numeric на Decimal"
+
+#: plpy_typeio.c:912
+#, c-format
+msgid "could not create bytes representation of Python object"
+msgstr "не вдалося створити байтову репрезентацію об'єкта Python"
+
+#: plpy_typeio.c:1049
+#, c-format
+msgid "could not create string representation of Python object"
+msgstr "не вдалося створити рядкову репрезентацію об'єкта Python"
+
+#: plpy_typeio.c:1060
+#, c-format
+msgid "could not convert Python object into cstring: Python string representation appears to contain null bytes"
+msgstr "не вдалося перетворити об'єкт Python на cstring: репрезентація рядка Python містить значення null-байти"
+
+#: plpy_typeio.c:1157
+#, c-format
+msgid "return value of function with array return type is not a Python sequence"
+msgstr "значення функції з масивом в якості результату не є послідовністю Python"
+
+#: plpy_typeio.c:1202
+#, c-format
+msgid "could not determine sequence length for function return value"
+msgstr "не вдалося визначити довжину послідовності для значення функція"
+
+#: plpy_typeio.c:1222 plpy_typeio.c:1237 plpy_typeio.c:1253
+#, c-format
+msgid "multidimensional arrays must have array expressions with matching dimensions"
+msgstr "для багатовимірних масивів повинні задаватись вирази з відповідними вимірами"
+
+#: plpy_typeio.c:1227
+#, c-format
+msgid "number of array dimensions exceeds the maximum allowed (%d)"
+msgstr "кількість вимірів масиву перевищує максимально дозволену (%d)"
+
+#: plpy_typeio.c:1329
+#, c-format
+msgid "malformed record literal: \"%s\""
+msgstr "невірно сформований літерал запису: \"%s\""
+
+#: plpy_typeio.c:1330
+#, c-format
+msgid "Missing left parenthesis."
+msgstr "Відсутня ліва дужка."
+
+#: plpy_typeio.c:1331 plpy_typeio.c:1532
+#, c-format
+msgid "To return a composite type in an array, return the composite type as a Python tuple, e.g., \"[('foo',)]\"."
+msgstr "Щоб повернути складений тип в масиві, треба повернути композитний тип як кортеж Python, наприклад, \"[('foo',)]\"."
+
+#: plpy_typeio.c:1378
+#, c-format
+msgid "key \"%s\" not found in mapping"
+msgstr "ключ \"%s\" не знайдено в зіставленні"
+
+#: plpy_typeio.c:1379
+#, c-format
+msgid "To return null in a column, add the value None to the mapping with the key named after the column."
+msgstr "Для повернення значення null в стовпці, додайте значення None з ключом, що дорівнює імені стовпця."
+
+#: plpy_typeio.c:1432
+#, c-format
+msgid "length of returned sequence did not match number of columns in row"
+msgstr "довжина повернутої послідовності не відповідає кількості стовпців у рядку"
+
+#: plpy_typeio.c:1530
+#, c-format
+msgid "attribute \"%s\" does not exist in Python object"
+msgstr "атрибут \"%s\" не існує в об'єкті Python"
+
+#: plpy_typeio.c:1533
+#, c-format
+msgid "To return null in a column, let the returned object have an attribute named after column with value None."
+msgstr "Щоб повернути null в стовпці, результуючий об'єкт має мати атрибут з іменем стовпця зі значенням None."
+
+#: plpy_util.c:31
+#, c-format
+msgid "could not convert Python Unicode object to bytes"
+msgstr "не вдалося конвертувати об'єкт Python Unicode в байти"
+
+#: plpy_util.c:37
+#, c-format
+msgid "could not extract bytes from encoded string"
+msgstr "не можливо отримати байт з закодованого рядка"
+
diff --git a/src/pl/plpython/po/vi.po b/src/pl/plpython/po/vi.po
new file mode 100644
index 0000000..58e0505
--- /dev/null
+++ b/src/pl/plpython/po/vi.po
@@ -0,0 +1,485 @@
+# LANGUAGE message translation file for plpython
+# Copyright (C) 2018 PostgreSQL Global Development Group
+# This file is distributed under the same license as the plpython (PostgreSQL) package.
+# FIRST AUTHOR <kakalot49@gmail.com>, 2018.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: plpython (PostgreSQL) 11\n"
+"Report-Msgid-Bugs-To: pgsql-bugs@postgresql.org\n"
+"POT-Creation-Date: 2018-04-22 12:08+0000\n"
+"PO-Revision-Date: 2018-05-06 22:57+0900\n"
+"Language-Team: <pgvn_translators@postgresql.vn>\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=1; plural=0;\n"
+"X-Generator: Poedit 2.0.6\n"
+"Last-Translator: Dang Minh Huong <kakalot49@gmail.com>\n"
+"Language: vi_VN\n"
+
+#: plpy_cursorobject.c:101
+#, c-format
+msgid "plpy.cursor expected a query or a plan"
+msgstr "plpy.cursor kỳ vọng một câu truy vấn hoặc một plan"
+
+#: plpy_cursorobject.c:184
+#, c-format
+msgid "plpy.cursor takes a sequence as its second argument"
+msgstr "plpy.cursor lấy một chuỗi làm đối số thứ hai"
+
+#: plpy_cursorobject.c:200 plpy_spi.c:211
+#, c-format
+msgid "could not execute plan"
+msgstr "không thể chạy plan"
+
+#: plpy_cursorobject.c:203 plpy_spi.c:214
+#, c-format
+msgid "Expected sequence of %d argument, got %d: %s"
+msgid_plural "Expected sequence of %d arguments, got %d: %s"
+msgstr[0] "Kỳ vọng chuỗi của đối số %d, đã nhận %d: %s"
+
+#: plpy_cursorobject.c:352
+#, c-format
+msgid "iterating a closed cursor"
+msgstr "lặp lại con trỏ đã đóng"
+
+#: plpy_cursorobject.c:360 plpy_cursorobject.c:426
+#, c-format
+msgid "iterating a cursor in an aborted subtransaction"
+msgstr "lặp lại một con trỏ trong một subtransaction đã bị hủy bỏ"
+
+#: plpy_cursorobject.c:418
+#, c-format
+msgid "fetch from a closed cursor"
+msgstr "fetch từ một con trỏ đã bị đóng"
+
+#: plpy_cursorobject.c:461 plpy_spi.c:409
+#, c-format
+msgid "query result has too many rows to fit in a Python list"
+msgstr "kết quả câu truy vấn có quá nhiều hàng để vừa với một danh sách Python"
+
+#: plpy_cursorobject.c:512
+#, c-format
+msgid "closing a cursor in an aborted subtransaction"
+msgstr "đóng một con trỏ trong một subtransaction bị hủy bỏ"
+
+#: plpy_elog.c:127 plpy_elog.c:128 plpy_plpymodule.c:559
+#, c-format
+msgid "%s"
+msgstr "%s"
+
+#: plpy_exec.c:142
+#, c-format
+msgid "unsupported set function return mode"
+msgstr "không hỗ trợ thiết lập hàm trả về mode"
+
+#: plpy_exec.c:143
+#, c-format
+msgid ""
+"PL/Python set-returning functions only support returning one value per call."
+msgstr ""
+"PL/Python hàm thiết lập-trả về chỉ hỗ trợ trả về một giá trị cho một lần gọi."
+
+#: plpy_exec.c:156
+#, c-format
+msgid "returned object cannot be iterated"
+msgstr "đối tượng trả về không thể được lặp lại"
+
+#: plpy_exec.c:157
+#, c-format
+msgid "PL/Python set-returning functions must return an iterable object."
+msgstr "PL/Python hàm thiết lập-trả về phải trả về một iterable object."
+
+#: plpy_exec.c:171
+#, c-format
+msgid "error fetching next item from iterator"
+msgstr "lỗi khi fetch item tiếp theo từ iterator"
+
+#: plpy_exec.c:214
+#, c-format
+msgid "PL/Python procedure did not return None"
+msgstr "Thủ tục PL/Python đã không trả về None"
+
+#: plpy_exec.c:218
+#, c-format
+msgid "PL/Python function with return type \"void\" did not return None"
+msgstr "Hàm PL/Python với kiểu trả về là \"void\" đã không trả về None"
+
+#: plpy_exec.c:374 plpy_exec.c:400
+#, c-format
+msgid "unexpected return value from trigger procedure"
+msgstr "không mong đợi giá trị trả về từ thủ tục trigger"
+
+#: plpy_exec.c:375
+#, c-format
+msgid "Expected None or a string."
+msgstr "Kỳ vọng None hoặc một chuỗi."
+
+#: plpy_exec.c:390
+#, c-format
+msgid ""
+"PL/Python trigger function returned \"MODIFY\" in a DELETE trigger -- ignored"
+msgstr ""
+"Hàm trigger PL/Python đã trả về \"MODIFY\" trong một DELETE trigger -- bỏ qua"
+
+#: plpy_exec.c:401
+#, c-format
+msgid "Expected None, \"OK\", \"SKIP\", or \"MODIFY\"."
+msgstr "Kỳ vọng None, \"OK\", \"SKIP\", hoặc \"MODIFY\"."
+
+#: plpy_exec.c:451
+#, c-format
+msgid "PyList_SetItem() failed, while setting up arguments"
+msgstr "Lỗi PyList_SetItem(), trong khi thiết lập đối số"
+
+#: plpy_exec.c:455
+#, c-format
+msgid "PyDict_SetItemString() failed, while setting up arguments"
+msgstr "Lỗi PyDict_SetItemString(), trong khi thiết lập đối số"
+
+#: plpy_exec.c:467
+#, c-format
+msgid ""
+"function returning record called in context that cannot accept type record"
+msgstr ""
+"hàm trả về bản ghi được gọi trong ngữ cảnh không thể chấp nhận kiểu bản ghi"
+
+#: plpy_exec.c:684
+#, c-format
+msgid "while creating return value"
+msgstr "trong khi tạo ra giá trị trả về"
+
+#: plpy_exec.c:909
+#, c-format
+msgid "TD[\"new\"] deleted, cannot modify row"
+msgstr "TD[\"new\"] đã bị xóa, không thể sửa đổi hàng"
+
+#: plpy_exec.c:914
+#, c-format
+msgid "TD[\"new\"] is not a dictionary"
+msgstr "TD[\"new\"] không phải là từ điển"
+
+#: plpy_exec.c:941
+#, c-format
+msgid "TD[\"new\"] dictionary key at ordinal position %d is not a string"
+msgstr "Khóa từ điển TD[\"new\"] ở vị trí thứ tự %d không phải là chuỗi"
+
+#: plpy_exec.c:948
+#, c-format
+msgid ""
+"key \"%s\" found in TD[\"new\"] does not exist as a column in the triggering "
+"row"
+msgstr ""
+"khóa \"%s\" được tìm thấy trong TD[\"new\"] không tồn tại như là trigger mức "
+"độ hàng"
+
+#: plpy_exec.c:953
+#, c-format
+msgid "cannot set system attribute \"%s\""
+msgstr "không thể thiết lập thuộc tính hệ thống \"%s\""
+
+#: plpy_exec.c:1011
+#, c-format
+msgid "while modifying trigger row"
+msgstr "trong khi sửa đổi trigger mức độ hàng"
+
+#: plpy_exec.c:1072
+#, c-format
+msgid "forcibly aborting a subtransaction that has not been exited"
+msgstr "buộc phải hủy bỏ một subtransaction chưa được thoát"
+
+#: plpy_main.c:125
+#, c-format
+msgid "multiple Python libraries are present in session"
+msgstr "có nhiều thư viện Python trong một phiên"
+
+#: plpy_main.c:126
+#, c-format
+msgid "Only one Python major version can be used in one session."
+msgstr "Chỉ có thể sử dụng một phiên bản chính của Python trong một phiên."
+
+#: plpy_main.c:142
+#, c-format
+msgid "untrapped error in initialization"
+msgstr "lỗi chưa được bẫy trong lúc khởi tạo"
+
+#: plpy_main.c:165
+#, c-format
+msgid "could not import \"__main__\" module"
+msgstr "không thể nhập mô-đun \"__main__\""
+
+#: plpy_main.c:174
+#, c-format
+msgid "could not initialize globals"
+msgstr "không thể khởi tạo biến global"
+
+#: plpy_main.c:399
+#, c-format
+msgid "PL/Python procedure \"%s\""
+msgstr "Thủ tục PL/Python \"%s\""
+
+#: plpy_main.c:402
+#, c-format
+msgid "PL/Python function \"%s\""
+msgstr "Hàm PL/Python \"%s\""
+
+#: plpy_main.c:410
+#, c-format
+msgid "PL/Python anonymous code block"
+msgstr "Khối mã ẩn danh PL/Python"
+
+#: plpy_plpymodule.c:192 plpy_plpymodule.c:195
+#, c-format
+msgid "could not import \"plpy\" module"
+msgstr "không thể nhập mô-đun \"plpy\""
+
+#: plpy_plpymodule.c:210
+#, c-format
+msgid "could not create the spiexceptions module"
+msgstr "không thể tạo mô-đun spiexceptions"
+
+#: plpy_plpymodule.c:218
+#, c-format
+msgid "could not add the spiexceptions module"
+msgstr "không thể thêm mô-đun spiexceptions"
+
+#: plpy_plpymodule.c:286
+#, c-format
+msgid "could not generate SPI exceptions"
+msgstr "không thể tạo exception SPI"
+
+#: plpy_plpymodule.c:454
+#, c-format
+msgid "could not unpack arguments in plpy.elog"
+msgstr "không thể giải nén đối số trong plpy.elog"
+
+#: plpy_plpymodule.c:463
+msgid "could not parse error message in plpy.elog"
+msgstr "không thể phân tích cú pháp thông điệp lỗi trong plpy.elog"
+
+#: plpy_plpymodule.c:480
+#, c-format
+msgid "argument 'message' given by name and position"
+msgstr "đối số 'message' được chỉ định theo tên và vị trí"
+
+#: plpy_plpymodule.c:507
+#, c-format
+msgid "'%s' is an invalid keyword argument for this function"
+msgstr "'%s' là đối số từ khóa không hợp lệ cho hàm này"
+
+#: plpy_plpymodule.c:518 plpy_plpymodule.c:524
+#, c-format
+msgid "invalid SQLSTATE code"
+msgstr "mã SQLSTATE không hợp lệ"
+
+#: plpy_procedure.c:230
+#, c-format
+msgid "trigger functions can only be called as triggers"
+msgstr "hàm trigger chỉ có thể được gọi như trigger"
+
+#: plpy_procedure.c:234
+#, c-format
+msgid "PL/Python functions cannot return type %s"
+msgstr "Hàm PL/Python không thể trả về kiểu %s"
+
+#: plpy_procedure.c:312
+#, c-format
+msgid "PL/Python functions cannot accept type %s"
+msgstr "Các hàm PL/Python không thể chấp nhận kiểu %s"
+
+#: plpy_procedure.c:402
+#, c-format
+msgid "could not compile PL/Python function \"%s\""
+msgstr "không thể biên dịch hàm PL/Python \"%s\""
+
+#: plpy_procedure.c:405
+#, c-format
+msgid "could not compile anonymous PL/Python code block"
+msgstr "không thể biên dịch khối mã ẩn danh PL/Python"
+
+#: plpy_resultobject.c:150 plpy_resultobject.c:176 plpy_resultobject.c:202
+#, c-format
+msgid "command did not produce a result set"
+msgstr "lệnh không tạo ra một tập hợp kết quả"
+
+#: plpy_spi.c:60
+#, c-format
+msgid "second argument of plpy.prepare must be a sequence"
+msgstr "đối số thứ hai của plpy.prepare phải là một chuỗi"
+
+#: plpy_spi.c:104
+#, c-format
+msgid "plpy.prepare: type name at ordinal position %d is not a string"
+msgstr "plpy.prepare: gõ tên tại vị trí thứ tự %d không phải là một chuỗi"
+
+#: plpy_spi.c:176
+#, c-format
+msgid "plpy.execute expected a query or a plan"
+msgstr "plpy.execute kỳ vọng một truy vấn hoặc một plan"
+
+#: plpy_spi.c:195
+#, c-format
+msgid "plpy.execute takes a sequence as its second argument"
+msgstr "plpy.execute lấy một chuỗi làm đối số thứ hai"
+
+#: plpy_spi.c:305
+#, c-format
+msgid "SPI_execute_plan failed: %s"
+msgstr "SPI_execute_plan lỗi: %s"
+
+#: plpy_spi.c:347
+#, c-format
+msgid "SPI_execute failed: %s"
+msgstr "SPI_execute lỗi: %s"
+
+#: plpy_subxactobject.c:122
+#, c-format
+msgid "this subtransaction has already been entered"
+msgstr "subtransaction này đã được nhập"
+
+#: plpy_subxactobject.c:128 plpy_subxactobject.c:186
+#, c-format
+msgid "this subtransaction has already been exited"
+msgstr "subtransaction này đã được thoát"
+
+#: plpy_subxactobject.c:180
+#, c-format
+msgid "this subtransaction has not been entered"
+msgstr "subtransaction này chưa được nhập"
+
+#: plpy_subxactobject.c:192
+#, c-format
+msgid "there is no subtransaction to exit from"
+msgstr "không có subtransaction để thoát khỏi"
+
+#: plpy_typeio.c:591
+#, c-format
+msgid "could not import a module for Decimal constructor"
+msgstr "không thể nhập mô-đun cho Decimal constructor"
+
+#: plpy_typeio.c:595
+#, c-format
+msgid "no Decimal attribute in module"
+msgstr "không có thuộc tính thập phân trong mô-đun"
+
+#: plpy_typeio.c:601
+#, c-format
+msgid "conversion from numeric to Decimal failed"
+msgstr "chuyển đổi từ numeric sang thập phân không thành công"
+
+#: plpy_typeio.c:908
+#, c-format
+msgid "could not create bytes representation of Python object"
+msgstr "không thể tạo đại diện cho của đối tượng Python"
+
+#: plpy_typeio.c:1056
+#, c-format
+msgid "could not create string representation of Python object"
+msgstr "không thể tạo ra chuỗi đại diện cho đối tượng Python"
+
+#: plpy_typeio.c:1067
+#, c-format
+msgid ""
+"could not convert Python object into cstring: Python string representation "
+"appears to contain null bytes"
+msgstr ""
+"không thể chuyển đổi đối tượng Python thành cstring: đại diện chuỗi Python "
+"chứa byte null"
+
+#: plpy_typeio.c:1176
+#, c-format
+msgid "number of array dimensions exceeds the maximum allowed (%d)"
+msgstr "số lượng hướng của mảng vượt quá số lượng tối đa cho phép (%d)"
+
+#: plpy_typeio.c:1180
+#, c-format
+msgid "could not determine sequence length for function return value"
+msgstr "không thể xác định độ dài chuỗi cho giá trị trả về hàm"
+
+#: plpy_typeio.c:1183 plpy_typeio.c:1187
+#, c-format
+msgid "array size exceeds the maximum allowed"
+msgstr "kích thước mảng vượt quá mức tối đa cho phép"
+
+#: plpy_typeio.c:1213
+#, c-format
+msgid ""
+"return value of function with array return type is not a Python sequence"
+msgstr ""
+"giá trị trả về của hàm với kiểu trả về là mảng không phải là một chuỗi Python"
+
+#: plpy_typeio.c:1259
+#, c-format
+msgid "wrong length of inner sequence: has length %d, but %d was expected"
+msgstr "sai độ dài của chuỗi bên trong: có độ dài %d, nhưng %d được mong đợi"
+
+#: plpy_typeio.c:1261
+#, c-format
+msgid ""
+"To construct a multidimensional array, the inner sequences must all have the "
+"same length."
+msgstr ""
+"Để xây dựng một mảng đa chiều, các chuỗi bên trong phải có cùng độ dài."
+
+#: plpy_typeio.c:1340
+#, c-format
+msgid "malformed record literal: \"%s\""
+msgstr "bản ghi literal không đúng định dạng: \"%s\""
+
+#: plpy_typeio.c:1341
+#, c-format
+msgid "Missing left parenthesis."
+msgstr "Thiếu dấu ngoặc đơn trái."
+
+#: plpy_typeio.c:1342 plpy_typeio.c:1543
+#, c-format
+msgid ""
+"To return a composite type in an array, return the composite type as a "
+"Python tuple, e.g., \"[('foo',)]\"."
+msgstr ""
+"Để trả về kiểu phức hợp trong một mảng, hãy trả về kiểu phức hợp dưới dạng "
+"một hàng Python, ví dụ: \"[('foo',)]\"."
+
+#: plpy_typeio.c:1389
+#, c-format
+msgid "key \"%s\" not found in mapping"
+msgstr "không tìm thấy khóa \"%s\" trong ánh xạ"
+
+#: plpy_typeio.c:1390
+#, c-format
+msgid ""
+"To return null in a column, add the value None to the mapping with the key "
+"named after the column."
+msgstr ""
+"Để trả về null trong một cột, thêm giá trị None vào ánh xạ với khóa được đặt "
+"tên sau cột."
+
+#: plpy_typeio.c:1443
+#, c-format
+msgid "length of returned sequence did not match number of columns in row"
+msgstr "độ dài của chuỗi được trả về không khớp với số cột trong hàng"
+
+#: plpy_typeio.c:1541
+#, c-format
+msgid "attribute \"%s\" does not exist in Python object"
+msgstr "thuộc tính \"%s\" không tồn tại trong đối tượng Python"
+
+#: plpy_typeio.c:1544
+#, c-format
+msgid ""
+"To return null in a column, let the returned object have an attribute named "
+"after column with value None."
+msgstr ""
+"Để trả về null trong một cột, hãy để đối tượng trả về có một thuộc tính được "
+"đặt tên sau cột với giá trị None."
+
+#: plpy_util.c:35
+#, c-format
+msgid "could not convert Python Unicode object to bytes"
+msgstr "không thể chuyển đổi đối tượng Python Unicode thành byte"
+
+#: plpy_util.c:41
+#, c-format
+msgid "could not extract bytes from encoded string"
+msgstr "không thể trích xuất byte từ chuỗi đã được mã hóa"
diff --git a/src/pl/plpython/po/zh_CN.po b/src/pl/plpython/po/zh_CN.po
new file mode 100644
index 0000000..264d4f5
--- /dev/null
+++ b/src/pl/plpython/po/zh_CN.po
@@ -0,0 +1,459 @@
+# LANGUAGE message translation file for plpython
+# Copyright (C) 2010 PostgreSQL Global Development Group
+# This file is distributed under the same license as the PostgreSQL package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, 2010.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: plpython (PostgreSQL) 14\n"
+"Report-Msgid-Bugs-To: pgsql-bugs@lists.postgresql.org\n"
+"POT-Creation-Date: 2021-08-14 05:38+0000\n"
+"PO-Revision-Date: 2021-08-16 18:00+0800\n"
+"Last-Translator: Jie Zhang <zhangjie2@fujitsu.com>\n"
+"Language-Team: Chinese (Simplified) <zhangjie2@fujitsu.com>\n"
+"Language: zh_CN\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=1; plural=0;\n"
+"X-Generator: Poedit 1.5.7\n"
+
+#: plpy_cursorobject.c:72
+#, c-format
+msgid "plpy.cursor expected a query or a plan"
+msgstr "plpy.cursor期望一个查询或一个计划"
+
+#: plpy_cursorobject.c:155
+#, c-format
+msgid "plpy.cursor takes a sequence as its second argument"
+msgstr "plpy.cursor将一个序列作为它的第二个参数"
+
+#: plpy_cursorobject.c:171 plpy_spi.c:207
+#, c-format
+msgid "could not execute plan"
+msgstr "无法执行计划"
+
+#: plpy_cursorobject.c:174 plpy_spi.c:210
+#, c-format
+msgid "Expected sequence of %d argument, got %d: %s"
+msgid_plural "Expected sequence of %d arguments, got %d: %s"
+msgstr[0] "期望%d序列参数,但是得到%d: %s"
+
+#: plpy_cursorobject.c:321
+#, c-format
+msgid "iterating a closed cursor"
+msgstr "遍历一个关闭的游标"
+
+#: plpy_cursorobject.c:329 plpy_cursorobject.c:395
+#, c-format
+msgid "iterating a cursor in an aborted subtransaction"
+msgstr "在终止的子事务里遍历一个游标"
+
+#: plpy_cursorobject.c:387
+#, c-format
+msgid "fetch from a closed cursor"
+msgstr "从关闭的游标里获取结果"
+
+#: plpy_cursorobject.c:430 plpy_spi.c:403
+#, c-format
+msgid "query result has too many rows to fit in a Python list"
+msgstr "查询结果中的行太多,无法放在一个Python列表中"
+
+#: plpy_cursorobject.c:482
+#, c-format
+msgid "closing a cursor in an aborted subtransaction"
+msgstr "在终止的子事务里关闭一个游标"
+
+#: plpy_elog.c:125 plpy_elog.c:126 plpy_plpymodule.c:548
+#, c-format
+msgid "%s"
+msgstr "%s"
+
+#: plpy_exec.c:139
+#, c-format
+msgid "unsupported set function return mode"
+msgstr "不支持集合函数返回模式"
+
+#: plpy_exec.c:140
+#, c-format
+msgid "PL/Python set-returning functions only support returning one value per call."
+msgstr "PL/Python集合返回函数只支持在每次调用时返回一个值。"
+
+#: plpy_exec.c:153
+#, c-format
+msgid "returned object cannot be iterated"
+msgstr "所返回的对象无法迭代"
+
+#: plpy_exec.c:154
+#, c-format
+msgid "PL/Python set-returning functions must return an iterable object."
+msgstr "PL/Python集合返回函数必须返回一个可迭代的对象."
+
+#: plpy_exec.c:168
+#, c-format
+msgid "error fetching next item from iterator"
+msgstr "当从迭代器中取回下一个成员时出现错误"
+
+#: plpy_exec.c:211
+#, c-format
+msgid "PL/Python procedure did not return None"
+msgstr "PL/Python过程没有返回None"
+
+#: plpy_exec.c:215
+#, c-format
+msgid "PL/Python function with return type \"void\" did not return None"
+msgstr "返回类型为\"void\"的PL/Python函数不返回None"
+
+#: plpy_exec.c:371 plpy_exec.c:397
+#, c-format
+msgid "unexpected return value from trigger procedure"
+msgstr "在触发器存储过程出现非期望的返回值"
+
+#: plpy_exec.c:372
+#, c-format
+msgid "Expected None or a string."
+msgstr "期望空值或一个字符串"
+
+#: plpy_exec.c:387
+#, c-format
+msgid "PL/Python trigger function returned \"MODIFY\" in a DELETE trigger -- ignored"
+msgstr "在DELETE触发器中的PL/Python 触发器函数返回 \"MODIFY\" -- 忽略"
+
+#: plpy_exec.c:398
+#, c-format
+msgid "Expected None, \"OK\", \"SKIP\", or \"MODIFY\"."
+msgstr "期望None, \"OK\", \"SKIP\", 或\"MODIFY\""
+
+#: plpy_exec.c:443
+#, c-format
+msgid "PyList_SetItem() failed, while setting up arguments"
+msgstr "当设置参数的同时, 执行PyList_SetItem()失败"
+
+#: plpy_exec.c:447
+#, c-format
+msgid "PyDict_SetItemString() failed, while setting up arguments"
+msgstr "当设置参数的同时, 执行PyDict_SetItemString()失败"
+
+#: plpy_exec.c:459
+#, c-format
+msgid "function returning record called in context that cannot accept type record"
+msgstr "返回值类型是记录的函数在不接受使用记录类型的环境中调用"
+
+#: plpy_exec.c:676
+#, c-format
+msgid "while creating return value"
+msgstr "同时在创建返回值"
+
+#: plpy_exec.c:910
+#, c-format
+msgid "TD[\"new\"] deleted, cannot modify row"
+msgstr "TD[\"new\"] 已删除,无法修改记录"
+
+#: plpy_exec.c:915
+#, c-format
+msgid "TD[\"new\"] is not a dictionary"
+msgstr "TD[\"new\"]不是一个字典"
+
+#: plpy_exec.c:942
+#, c-format
+msgid "TD[\"new\"] dictionary key at ordinal position %d is not a string"
+msgstr "在顺序位置%d的TD[\"new\"]字典键值不是字符串"
+
+#: plpy_exec.c:949
+#, c-format
+msgid "key \"%s\" found in TD[\"new\"] does not exist as a column in the triggering row"
+msgstr "在 TD[\"new\"]中找到的键 \"%s\"在正在触发的记录中不是作为列而存在."
+
+#: plpy_exec.c:954
+#, c-format
+msgid "cannot set system attribute \"%s\""
+msgstr "不能设置系统属性\"%s\""
+
+#: plpy_exec.c:959
+#, c-format
+msgid "cannot set generated column \"%s\""
+msgstr "无法设置生成的列 \"%s\""
+
+#: plpy_exec.c:1017
+#, c-format
+msgid "while modifying trigger row"
+msgstr "同时正在修改触发器记录"
+
+#: plpy_exec.c:1075
+#, c-format
+msgid "forcibly aborting a subtransaction that has not been exited"
+msgstr "强行终止一个还未退出的子事务"
+
+#: plpy_main.c:121
+#, c-format
+msgid "multiple Python libraries are present in session"
+msgstr "会话中存在多个Python库"
+
+#: plpy_main.c:122
+#, c-format
+msgid "Only one Python major version can be used in one session."
+msgstr "一个会话中只能使用一个Python主版本."
+
+#: plpy_main.c:138
+#, c-format
+msgid "untrapped error in initialization"
+msgstr "在初始化过程中出现无法捕获的错误"
+
+#: plpy_main.c:161
+#, c-format
+msgid "could not import \"__main__\" module"
+msgstr "无法导入模块\"__main__\" "
+
+#: plpy_main.c:170
+#, c-format
+msgid "could not initialize globals"
+msgstr "无法初始化全局变量"
+
+#: plpy_main.c:393
+#, c-format
+msgid "PL/Python procedure \"%s\""
+msgstr "PL/Python过程\"%s\""
+
+#: plpy_main.c:396
+#, c-format
+msgid "PL/Python function \"%s\""
+msgstr "PL/Python函数\"%s\""
+
+#: plpy_main.c:404
+#, c-format
+msgid "PL/Python anonymous code block"
+msgstr "PL/Python匿名代码块"
+
+#: plpy_plpymodule.c:182 plpy_plpymodule.c:185
+#, c-format
+msgid "could not import \"plpy\" module"
+msgstr "无法导入模块\"plpy\" "
+
+#: plpy_plpymodule.c:200
+#, c-format
+msgid "could not create the spiexceptions module"
+msgstr "无法创建spiexceptions模块"
+
+#: plpy_plpymodule.c:208
+#, c-format
+msgid "could not add the spiexceptions module"
+msgstr "无法添加spiexceptions模块 "
+
+#: plpy_plpymodule.c:275
+#, c-format
+msgid "could not generate SPI exceptions"
+msgstr "无法产生SPI异常"
+
+#: plpy_plpymodule.c:443
+#, c-format
+msgid "could not unpack arguments in plpy.elog"
+msgstr "无法解析plpy.elog中的参数"
+
+#: plpy_plpymodule.c:452
+msgid "could not parse error message in plpy.elog"
+msgstr "无法解析在plpy.elog中的错误消息"
+
+#: plpy_plpymodule.c:469
+#, c-format
+msgid "argument 'message' given by name and position"
+msgstr "由名称和位置提供的参数'message'"
+
+#: plpy_plpymodule.c:496
+#, c-format
+msgid "'%s' is an invalid keyword argument for this function"
+msgstr "对于这个函数,'%s'是一个无效的关键词参数"
+
+#: plpy_plpymodule.c:507 plpy_plpymodule.c:513
+#, c-format
+msgid "invalid SQLSTATE code"
+msgstr "无效的SQLSTATE代码"
+
+#: plpy_procedure.c:225
+#, c-format
+msgid "trigger functions can only be called as triggers"
+msgstr "触发器函数只能以触发器的形式调用"
+
+#: plpy_procedure.c:229
+#, c-format
+msgid "PL/Python functions cannot return type %s"
+msgstr "PL/Python函数不能返回类型%s"
+
+#: plpy_procedure.c:307
+#, c-format
+msgid "PL/Python functions cannot accept type %s"
+msgstr "PL/Python函数不能接受类型%s"
+
+#: plpy_procedure.c:397
+#, c-format
+msgid "could not compile PL/Python function \"%s\""
+msgstr "无法编译PL/Python函数\"%s\""
+
+#: plpy_procedure.c:400
+#, c-format
+msgid "could not compile anonymous PL/Python code block"
+msgstr "无法编译PL/Python中的匿名代码块"
+
+#: plpy_resultobject.c:117 plpy_resultobject.c:143 plpy_resultobject.c:169
+#, c-format
+msgid "command did not produce a result set"
+msgstr "命令没有产生结果集"
+
+#: plpy_spi.c:56
+#, c-format
+msgid "second argument of plpy.prepare must be a sequence"
+msgstr "plpy.prepare的第二个参数必须是一个序列"
+
+#: plpy_spi.c:100
+#, c-format
+msgid "plpy.prepare: type name at ordinal position %d is not a string"
+msgstr "plpy.prepare: 在顺序位置%d的类型名称不是string"
+
+#: plpy_spi.c:172
+#, c-format
+msgid "plpy.execute expected a query or a plan"
+msgstr "plpy.execute期望一个查询或一个计划"
+
+#: plpy_spi.c:191
+#, c-format
+msgid "plpy.execute takes a sequence as its second argument"
+msgstr "plpy.execute将一个序列作为它的第二个参数"
+
+#: plpy_spi.c:299
+#, c-format
+msgid "SPI_execute_plan failed: %s"
+msgstr "执行SPI_execute_plan失败: %s"
+
+#: plpy_spi.c:341
+#, c-format
+msgid "SPI_execute failed: %s"
+msgstr "SPI_execute执行失败: %s"
+
+#: plpy_subxactobject.c:92
+#, c-format
+msgid "this subtransaction has already been entered"
+msgstr "已经进入该子事务"
+
+#: plpy_subxactobject.c:98 plpy_subxactobject.c:156
+#, c-format
+msgid "this subtransaction has already been exited"
+msgstr "已经退出该子事务"
+
+#: plpy_subxactobject.c:150
+#, c-format
+msgid "this subtransaction has not been entered"
+msgstr "该子事务仍没有进入"
+
+#: plpy_subxactobject.c:162
+#, c-format
+msgid "there is no subtransaction to exit from"
+msgstr "没有子事务可以退出"
+
+#: plpy_typeio.c:587
+#, c-format
+msgid "could not import a module for Decimal constructor"
+msgstr "无法为十进制构造函数导入模块"
+
+#: plpy_typeio.c:591
+#, c-format
+msgid "no Decimal attribute in module"
+msgstr "模块中没有小数位属性"
+
+#: plpy_typeio.c:597
+#, c-format
+msgid "conversion from numeric to Decimal failed"
+msgstr "由numeric数值到Decimal小数转换失败"
+
+#: plpy_typeio.c:911
+#, c-format
+msgid "could not create bytes representation of Python object"
+msgstr "无法创建Python对象的字节表达式"
+
+#: plpy_typeio.c:1056
+#, c-format
+msgid "could not create string representation of Python object"
+msgstr "无法创建Python对象的字符串表达式"
+
+#: plpy_typeio.c:1067
+#, c-format
+msgid "could not convert Python object into cstring: Python string representation appears to contain null bytes"
+msgstr "无法将Python对象转换为cstring: Python字符串表达式可能包含空字节"
+
+#: plpy_typeio.c:1178
+#, c-format
+msgid "number of array dimensions exceeds the maximum allowed (%d)"
+msgstr "数组的维数超过最大允许值(%d)"
+
+#: plpy_typeio.c:1183
+#, c-format
+msgid "could not determine sequence length for function return value"
+msgstr "无法确定函数返回值的序列长度"
+
+#: plpy_typeio.c:1188 plpy_typeio.c:1194
+#, c-format
+msgid "array size exceeds the maximum allowed"
+msgstr "数组的大小超过了最大允许值"
+
+#: plpy_typeio.c:1222
+#, c-format
+msgid "return value of function with array return type is not a Python sequence"
+msgstr "带有数组返回类型的函数返回值不是一个Python序列"
+
+#: plpy_typeio.c:1269
+#, c-format
+msgid "wrong length of inner sequence: has length %d, but %d was expected"
+msgstr "内部序列的长度错误:长度为%d,但应为%d"
+
+#: plpy_typeio.c:1271
+#, c-format
+msgid "To construct a multidimensional array, the inner sequences must all have the same length."
+msgstr "要构造多维数组,内部序列的长度必须相同."
+
+#: plpy_typeio.c:1350
+#, c-format
+msgid "malformed record literal: \"%s\""
+msgstr "有缺陷的记录常量: \"%s\""
+
+#: plpy_typeio.c:1351
+#, c-format
+msgid "Missing left parenthesis."
+msgstr "缺少一个左括弧"
+
+#: plpy_typeio.c:1352 plpy_typeio.c:1553
+#, c-format
+msgid "To return a composite type in an array, return the composite type as a Python tuple, e.g., \"[('foo',)]\"."
+msgstr "要返回数组中的复合类型,请将复合类型作为Python元组返回,例如 \"[('foo',)]\"."
+
+#: plpy_typeio.c:1399
+#, c-format
+msgid "key \"%s\" not found in mapping"
+msgstr "在映射中没有找到键\"%s\""
+
+#: plpy_typeio.c:1400
+#, c-format
+msgid "To return null in a column, add the value None to the mapping with the key named after the column."
+msgstr "为了在一列中返回空值, 需要在列的后面对带有已命名键的映射添加值None"
+
+#: plpy_typeio.c:1453
+#, c-format
+msgid "length of returned sequence did not match number of columns in row"
+msgstr "所返回序列的长度与在记录中列的数量不匹配"
+
+#: plpy_typeio.c:1551
+#, c-format
+msgid "attribute \"%s\" does not exist in Python object"
+msgstr "在Python对象中不存在属性\"%s\""
+
+#: plpy_typeio.c:1554
+#, c-format
+msgid "To return null in a column, let the returned object have an attribute named after column with value None."
+msgstr "为了在一列中返回空值, 需要让返回的对象在带有值None的列后面的带有已命名属性"
+
+#: plpy_util.c:31
+#, c-format
+msgid "could not convert Python Unicode object to bytes"
+msgstr "无法将Python中以Unicode编码的对象转换为PostgreSQL服务器字节码"
+
+#: plpy_util.c:37
+#, c-format
+msgid "could not extract bytes from encoded string"
+msgstr "无法从已编码字符串里提取相应字节码值"
diff --git a/src/pl/plpython/po/zh_TW.po b/src/pl/plpython/po/zh_TW.po
new file mode 100644
index 0000000..d698881
--- /dev/null
+++ b/src/pl/plpython/po/zh_TW.po
@@ -0,0 +1,496 @@
+# Traditional Chinese message translation file for plpython
+# Copyright (C) 2023 PostgreSQL Global Development Group
+# This file is distributed under the same license as the plpython (PostgreSQL) package.
+# Zhenbang Wei <znbang@gmail.com>, 2011.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: plpython (PostgreSQL) 16\n"
+"Report-Msgid-Bugs-To: pgsql-bugs@lists.postgresql.org\n"
+"POT-Creation-Date: 2023-09-11 20:38+0000\n"
+"PO-Revision-Date: 2023-11-06 08:50+0800\n"
+"Last-Translator: Zhenbang Wei <znbang@gmail.com>\n"
+"Language-Team: \n"
+"Language: zh_TW\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=1; plural=0;\n"
+"X-Generator: Poedit 3.4.1\n"
+
+#: plpy_cursorobject.c:72
+#, c-format
+msgid "plpy.cursor expected a query or a plan"
+msgstr "plpy.cursor 預期查詢或計劃"
+
+#: plpy_cursorobject.c:155
+#, c-format
+msgid "plpy.cursor takes a sequence as its second argument"
+msgstr "plpy.execute 接受序列做為第二個參數"
+
+#: plpy_cursorobject.c:171 plpy_spi.c:205
+#, c-format
+msgid "could not execute plan"
+msgstr "無法執行計畫"
+
+#: plpy_cursorobject.c:174 plpy_spi.c:208
+#, c-format
+msgid "Expected sequence of %d argument, got %d: %s"
+msgid_plural "Expected sequence of %d arguments, got %d: %s"
+msgstr[0] "預期 %d 個參數序列,收到 %d 個: %s"
+
+# sql_help.h:345
+#: plpy_cursorobject.c:321
+#, c-format
+msgid "iterating a closed cursor"
+msgstr "迭代已關閉的游標"
+
+# postmaster/postmaster.c:1832
+#: plpy_cursorobject.c:329 plpy_cursorobject.c:395
+#, c-format
+msgid "iterating a cursor in an aborted subtransaction"
+msgstr "在已中止的子交易中迭代游標"
+
+# sql_help.h:109
+#: plpy_cursorobject.c:387
+#, c-format
+msgid "fetch from a closed cursor"
+msgstr "從已關閉的游標中擷取資料"
+
+#: plpy_cursorobject.c:430 plpy_spi.c:401
+#, c-format
+msgid "query result has too many rows to fit in a Python list"
+msgstr "查詢結果的列數過多,無法放入 Python 串列"
+
+# postmaster/postmaster.c:1832
+#: plpy_cursorobject.c:482
+#, c-format
+msgid "closing a cursor in an aborted subtransaction"
+msgstr "在已中止的子交易中關閉游標"
+
+# commands/vacuum.c:2258 commands/vacuumlazy.c:489 commands/vacuumlazy.c:770
+# nodes/print.c:86 storage/lmgr/deadlock.c:888 tcop/postgres.c:3285
+#: plpy_elog.c:125 plpy_elog.c:126 plpy_plpymodule.c:530
+#, c-format
+msgid "%s"
+msgstr "%s"
+
+#: plpy_exec.c:139
+#, c-format
+msgid "unsupported set function return mode"
+msgstr "不支援的集合函數回傳模式"
+
+#: plpy_exec.c:140
+#, c-format
+msgid "PL/Python set-returning functions only support returning one value per call."
+msgstr "PL/Python 的集合回傳函數僅支援每次呼叫回傳一個值。"
+
+#: plpy_exec.c:153
+#, c-format
+msgid "returned object cannot be iterated"
+msgstr "無法迭代回傳的物件"
+
+#: plpy_exec.c:154
+#, c-format
+msgid "PL/Python set-returning functions must return an iterable object."
+msgstr "PL/Python 的集合回傳函數必須回傳一個可迭代的物件。"
+
+#: plpy_exec.c:168
+#, c-format
+msgid "error fetching next item from iterator"
+msgstr "從迭代器取得下一個項目時發生錯誤"
+
+#: plpy_exec.c:211
+#, c-format
+msgid "PL/Python procedure did not return None"
+msgstr "PL/Python 程序未回傳 None"
+
+#: plpy_exec.c:215
+#, c-format
+msgid "PL/Python function with return type \"void\" did not return None"
+msgstr "PL/Python 函數的回傳型別為 \"void\" 但未回傳 None"
+
+#: plpy_exec.c:369 plpy_exec.c:393
+#, c-format
+msgid "unexpected return value from trigger procedure"
+msgstr "觸發程序回傳非預期的值"
+
+#: plpy_exec.c:370
+#, c-format
+msgid "Expected None or a string."
+msgstr "預期 None 或字串。"
+
+#: plpy_exec.c:383
+#, c-format
+msgid "PL/Python trigger function returned \"MODIFY\" in a DELETE trigger -- ignored"
+msgstr "PL/Python 觸發函數在 DELETE 觸發器中回傳 \"MODIFY\" -- 已忽略"
+
+#: plpy_exec.c:394
+#, c-format
+msgid "Expected None, \"OK\", \"SKIP\", or \"MODIFY\"."
+msgstr "預期 None 或 \"OK\" 或 \"SKIP\" 或 \"MODIFY\"。"
+
+#: plpy_exec.c:444
+#, c-format
+msgid "PyList_SetItem() failed, while setting up arguments"
+msgstr "設定 PyList_SetItem() 的參數時失敗"
+
+#: plpy_exec.c:448
+#, c-format
+msgid "PyDict_SetItemString() failed, while setting up arguments"
+msgstr "設定 PyDict_SetItemString() 的參數時失敗"
+
+#: plpy_exec.c:460
+#, c-format
+msgid "function returning record called in context that cannot accept type record"
+msgstr "在不接受紀錄型別的 context 中呼叫回傳紀錄的函數"
+
+#: plpy_exec.c:677
+#, c-format
+msgid "while creating return value"
+msgstr "在建立回傳值時"
+
+#: plpy_exec.c:924
+#, c-format
+msgid "TD[\"new\"] deleted, cannot modify row"
+msgstr "TD[\"new\"] 已刪除,無法修改資料列"
+
+#: plpy_exec.c:929
+#, c-format
+msgid "TD[\"new\"] is not a dictionary"
+msgstr "TD[\"new\"] 不是字典"
+
+#: plpy_exec.c:954
+#, c-format
+msgid "TD[\"new\"] dictionary key at ordinal position %d is not a string"
+msgstr "位於順序位置 %d 的 TD[\"new\"] 字典鍵不是字串"
+
+#: plpy_exec.c:961
+#, c-format
+msgid "key \"%s\" found in TD[\"new\"] does not exist as a column in the triggering row"
+msgstr "TD[\"new\"] 中的鍵 \"%s\" 不在觸發資料列的欄位中"
+
+# commands/tablecmds.c:5425
+#: plpy_exec.c:966
+#, c-format
+msgid "cannot set system attribute \"%s\""
+msgstr "無法設定系統屬性 \"%s\""
+
+# commands/tablecmds.c:4580
+#: plpy_exec.c:971
+#, c-format
+msgid "cannot set generated column \"%s\""
+msgstr "無法設定產生的欄位 \"%s\""
+
+#: plpy_exec.c:1029
+#, c-format
+msgid "while modifying trigger row"
+msgstr "在修改觸發資料列時"
+
+#: plpy_exec.c:1087
+#, c-format
+msgid "forcibly aborting a subtransaction that has not been exited"
+msgstr "強制中止尚未退出的子交易"
+
+#: plpy_main.c:109
+#, c-format
+msgid "multiple Python libraries are present in session"
+msgstr "工作階段中有多個 Python 程式庫"
+
+#: plpy_main.c:110
+#, c-format
+msgid "Only one Python major version can be used in one session."
+msgstr "同一個工作階段中只能使用一個 Python 主要版本。"
+
+#: plpy_main.c:122
+#, c-format
+msgid "untrapped error in initialization"
+msgstr "初始化中未被捕獲的錯誤"
+
+#: plpy_main.c:145
+#, c-format
+msgid "could not import \"__main__\" module"
+msgstr "無法匯入 \"__main__\" 模組"
+
+#: plpy_main.c:154
+#, c-format
+msgid "could not initialize globals"
+msgstr "無法初始化全域變數"
+
+#: plpy_main.c:352
+#, c-format
+msgid "PL/Python procedure \"%s\""
+msgstr "PL/Python 程序 \"%s\""
+
+#: plpy_main.c:355
+#, c-format
+msgid "PL/Python function \"%s\""
+msgstr "PL/Python 函數 \"%s\""
+
+#: plpy_main.c:363
+#, c-format
+msgid "PL/Python anonymous code block"
+msgstr "PL/Python 匿名程式區塊"
+
+#: plpy_plpymodule.c:168 plpy_plpymodule.c:171
+#, c-format
+msgid "could not import \"plpy\" module"
+msgstr "無法匯入 \"plpy\" 模組"
+
+#: plpy_plpymodule.c:182
+#, c-format
+msgid "could not create the spiexceptions module"
+msgstr "無法建立 spiexceptions 模組"
+
+#: plpy_plpymodule.c:190
+#, c-format
+msgid "could not add the spiexceptions module"
+msgstr "無法新增 spiexceptions 模組"
+
+# access/transam/xlog.c:4111
+#: plpy_plpymodule.c:257
+#, c-format
+msgid "could not generate SPI exceptions"
+msgstr "無法產生 SPI 例外"
+
+#: plpy_plpymodule.c:425
+#, c-format
+msgid "could not unpack arguments in plpy.elog"
+msgstr "無法解析 plpy.elog 中的參數"
+
+#: plpy_plpymodule.c:434
+msgid "could not parse error message in plpy.elog"
+msgstr "無法解析 plpy.elog 中的錯誤訊息"
+
+#: plpy_plpymodule.c:451
+#, c-format
+msgid "argument 'message' given by name and position"
+msgstr "參數 'message' 被同時以名稱和位置給定"
+
+# commands/define.c:279
+#: plpy_plpymodule.c:478
+#, c-format
+msgid "'%s' is an invalid keyword argument for this function"
+msgstr "這個函數的 '%s' 是無效關鍵字參數"
+
+#: plpy_plpymodule.c:489 plpy_plpymodule.c:495
+#, c-format
+msgid "invalid SQLSTATE code"
+msgstr "無效的 SQLSTATE 碼"
+
+#: plpy_procedure.c:225
+#, c-format
+msgid "trigger functions can only be called as triggers"
+msgstr "觸發函數只能作為觸發器被呼叫"
+
+#: plpy_procedure.c:229
+#, c-format
+msgid "PL/Python functions cannot return type %s"
+msgstr "PL/Python 函數無法回傳型別 %s"
+
+#: plpy_procedure.c:307
+#, c-format
+msgid "PL/Python functions cannot accept type %s"
+msgstr "PL/Python 函數不能接受型別 %s"
+
+#: plpy_procedure.c:395
+#, c-format
+msgid "could not compile PL/Python function \"%s\""
+msgstr "無法編譯 PL/Python 函數 \"%s\""
+
+#: plpy_procedure.c:398
+#, c-format
+msgid "could not compile anonymous PL/Python code block"
+msgstr "無法編譯匿名 PL/Python 程式區塊"
+
+#: plpy_resultobject.c:117 plpy_resultobject.c:143 plpy_resultobject.c:169
+#, c-format
+msgid "command did not produce a result set"
+msgstr "命令未產生結果集"
+
+#: plpy_spi.c:56
+#, c-format
+msgid "second argument of plpy.prepare must be a sequence"
+msgstr "plpy.prepare 的第二個參數必須是序列"
+
+#: plpy_spi.c:98
+#, c-format
+msgid "plpy.prepare: type name at ordinal position %d is not a string"
+msgstr "plpy.prepare: 順序位置 %d 的型別名稱不是字串"
+
+#: plpy_spi.c:170
+#, c-format
+msgid "plpy.execute expected a query or a plan"
+msgstr "plpy.execute 預期查詢或計畫"
+
+#: plpy_spi.c:189
+#, c-format
+msgid "plpy.execute takes a sequence as its second argument"
+msgstr "plpy.execute 接受序列做為第二個參數"
+
+#: plpy_spi.c:297
+#, c-format
+msgid "SPI_execute_plan failed: %s"
+msgstr "SPI_execute_plan 失敗: %s"
+
+#: plpy_spi.c:339
+#, c-format
+msgid "SPI_execute failed: %s"
+msgstr "SPI_execute 失敗: %s"
+
+#: plpy_subxactobject.c:92
+#, c-format
+msgid "this subtransaction has already been entered"
+msgstr "已進入這個子交易"
+
+#: plpy_subxactobject.c:98 plpy_subxactobject.c:156
+#, c-format
+msgid "this subtransaction has already been exited"
+msgstr "已結束這個子交易"
+
+#: plpy_subxactobject.c:150
+#, c-format
+msgid "this subtransaction has not been entered"
+msgstr "尚未進入這個子交易"
+
+#: plpy_subxactobject.c:162
+#, c-format
+msgid "there is no subtransaction to exit from"
+msgstr "無需結束任何子交易"
+
+#: plpy_typeio.c:588
+#, c-format
+msgid "could not import a module for Decimal constructor"
+msgstr "無法匯入 Decimal 建構子的模組"
+
+#: plpy_typeio.c:592
+#, c-format
+msgid "no Decimal attribute in module"
+msgstr "模組中找不到 Decimal 屬性"
+
+#: plpy_typeio.c:598
+#, c-format
+msgid "conversion from numeric to Decimal failed"
+msgstr "從數值轉換為 Decimal 失敗"
+
+#: plpy_typeio.c:912
+#, c-format
+msgid "could not create bytes representation of Python object"
+msgstr "無法建立 Python 物件的位元組表示"
+
+#: plpy_typeio.c:1049
+#, c-format
+msgid "could not create string representation of Python object"
+msgstr "無法建立 Python 物件的字串表示"
+
+#: plpy_typeio.c:1060
+#, c-format
+msgid "could not convert Python object into cstring: Python string representation appears to contain null bytes"
+msgstr "無法將 Python 物件轉換為 cstring: Python 字串表示中似乎包含 null 位元組"
+
+#: plpy_typeio.c:1157
+#, c-format
+msgid "return value of function with array return type is not a Python sequence"
+msgstr "回傳型別是陣列的函數的回傳值並非 Python 序列"
+
+# access/common/tupdesc.c:679
+#: plpy_typeio.c:1202
+#, c-format
+msgid "could not determine sequence length for function return value"
+msgstr "無法確定函數回傳值的序列長度"
+
+# executor/execQual.c:2085 utils/adt/arrayfuncs.c:507
+#: plpy_typeio.c:1222 plpy_typeio.c:1237 plpy_typeio.c:1253
+#, c-format
+msgid "multidimensional arrays must have array expressions with matching dimensions"
+msgstr "多維陣列必須有相同維度的陣列表達式"
+
+# executor/execQual.c:257 executor/execQual.c:285 executor/execQual.c:2065
+# utils/adt/array_userfuncs.c:362 utils/adt/arrayfuncs.c:216
+# utils/adt/arrayfuncs.c:472 utils/adt/arrayfuncs.c:1153
+# utils/adt/arrayfuncs.c:2421
+#: plpy_typeio.c:1227
+#, c-format
+msgid "number of array dimensions exceeds the maximum allowed (%d)"
+msgstr "陣列維度數超過允許的最大值(%d)"
+
+# utils/adt/rowtypes.c:125 utils/adt/rowtypes.c:152 utils/adt/rowtypes.c:176
+# utils/adt/rowtypes.c:184 utils/adt/rowtypes.c:234 utils/adt/rowtypes.c:242
+#: plpy_typeio.c:1329
+#, c-format
+msgid "malformed record literal: \"%s\""
+msgstr "格式錯誤的記錄文字: \"%s\""
+
+# utils/adt/rowtypes.c:126
+#: plpy_typeio.c:1330
+#, c-format
+msgid "Missing left parenthesis."
+msgstr "缺少左括號。"
+
+#: plpy_typeio.c:1331 plpy_typeio.c:1532
+#, c-format
+msgid "To return a composite type in an array, return the composite type as a Python tuple, e.g., \"[('foo',)]\"."
+msgstr "若要在陣列中回傳複合型別,請將複合型別回傳為 Python 的 tuple,例如: \"[('foo',)]\"。"
+
+#: plpy_typeio.c:1378
+#, c-format
+msgid "key \"%s\" not found in mapping"
+msgstr "對照表中找不到鍵 \"%s\""
+
+#: plpy_typeio.c:1379
+#, c-format
+msgid "To return null in a column, add the value None to the mapping with the key named after the column."
+msgstr "若要在欄位回傳 null,以欄位名稱為鍵值將 None 加入對照表。"
+
+#: plpy_typeio.c:1432
+#, c-format
+msgid "length of returned sequence did not match number of columns in row"
+msgstr "回傳的序列長度與資料列中的欄位數不一致"
+
+#: plpy_typeio.c:1530
+#, c-format
+msgid "attribute \"%s\" does not exist in Python object"
+msgstr "Python 物件中不存在屬性 \"%s\""
+
+#: plpy_typeio.c:1533
+#, c-format
+msgid "To return null in a column, let the returned object have an attribute named after column with value None."
+msgstr "若要在欄位回傳 null,以欄位名稱為回傳物件的屬性名稱並以 None 為值。"
+
+#: plpy_util.c:31
+#, c-format
+msgid "could not convert Python Unicode object to bytes"
+msgstr "無法將 Python 的 Unicode 物件轉換為位元組"
+
+#: plpy_util.c:37
+#, c-format
+msgid "could not extract bytes from encoded string"
+msgstr "無法從編碼字串中提取位元組"
+
+#~ msgid "PL/Python only supports one-dimensional arrays."
+#~ msgstr "PL/Python 只支援一維陣列"
+
+#~ msgid "Start a new session to use a different Python major version."
+#~ msgstr "用不同 Python 主版號開始新 session"
+
+#, c-format
+#~ msgid "This session has previously used Python major version %d, and it is now attempting to use Python major version %d."
+#~ msgstr "session 原來用 Python 主版號 %d,現在嘗試用 Python 主版號 %d。"
+
+#~ msgid "cannot convert multidimensional array to Python list"
+#~ msgstr "無法將多維陣列轉成 Python list"
+
+#~ msgid "could not create new dictionary"
+#~ msgstr "無法建立新字典"
+
+#~ msgid "could not create new dictionary while building trigger arguments"
+#~ msgstr "建立觸發程序參數時無法建立新字典"
+
+#~ msgid "plan.status takes no arguments"
+#~ msgstr "plan.status 不接受參數"
+
+#~ msgid "plpy.prepare does not support composite types"
+#~ msgstr "plpy.prepare 不支援複合型別"
+
+#~ msgid "unrecognized error in PLy_spi_execute_fetch_result"
+#~ msgstr "PLy_spi_execute_fetch_result 有非預期錯誤"
diff --git a/src/pl/plpython/spiexceptions.h b/src/pl/plpython/spiexceptions.h
new file mode 100644
index 0000000..999e6bb
--- /dev/null
+++ b/src/pl/plpython/spiexceptions.h
@@ -0,0 +1,998 @@
+/* autogenerated from src/backend/utils/errcodes.txt, do not edit */
+/* there is deliberately not an #ifndef SPIEXCEPTIONS_H here */
+
+{
+ "spiexceptions.SqlStatementNotYetComplete", "SqlStatementNotYetComplete", ERRCODE_SQL_STATEMENT_NOT_YET_COMPLETE
+},
+
+{
+ "spiexceptions.ConnectionException", "ConnectionException", ERRCODE_CONNECTION_EXCEPTION
+},
+
+{
+ "spiexceptions.ConnectionDoesNotExist", "ConnectionDoesNotExist", ERRCODE_CONNECTION_DOES_NOT_EXIST
+},
+
+{
+ "spiexceptions.ConnectionFailure", "ConnectionFailure", ERRCODE_CONNECTION_FAILURE
+},
+
+{
+ "spiexceptions.SqlclientUnableToEstablishSqlconnection", "SqlclientUnableToEstablishSqlconnection", ERRCODE_SQLCLIENT_UNABLE_TO_ESTABLISH_SQLCONNECTION
+},
+
+{
+ "spiexceptions.SqlserverRejectedEstablishmentOfSqlconnection", "SqlserverRejectedEstablishmentOfSqlconnection", ERRCODE_SQLSERVER_REJECTED_ESTABLISHMENT_OF_SQLCONNECTION
+},
+
+{
+ "spiexceptions.TransactionResolutionUnknown", "TransactionResolutionUnknown", ERRCODE_TRANSACTION_RESOLUTION_UNKNOWN
+},
+
+{
+ "spiexceptions.ProtocolViolation", "ProtocolViolation", ERRCODE_PROTOCOL_VIOLATION
+},
+
+{
+ "spiexceptions.TriggeredActionException", "TriggeredActionException", ERRCODE_TRIGGERED_ACTION_EXCEPTION
+},
+
+{
+ "spiexceptions.FeatureNotSupported", "FeatureNotSupported", ERRCODE_FEATURE_NOT_SUPPORTED
+},
+
+{
+ "spiexceptions.InvalidTransactionInitiation", "InvalidTransactionInitiation", ERRCODE_INVALID_TRANSACTION_INITIATION
+},
+
+{
+ "spiexceptions.LocatorException", "LocatorException", ERRCODE_LOCATOR_EXCEPTION
+},
+
+{
+ "spiexceptions.InvalidLocatorSpecification", "InvalidLocatorSpecification", ERRCODE_L_E_INVALID_SPECIFICATION
+},
+
+{
+ "spiexceptions.InvalidGrantor", "InvalidGrantor", ERRCODE_INVALID_GRANTOR
+},
+
+{
+ "spiexceptions.InvalidGrantOperation", "InvalidGrantOperation", ERRCODE_INVALID_GRANT_OPERATION
+},
+
+{
+ "spiexceptions.InvalidRoleSpecification", "InvalidRoleSpecification", ERRCODE_INVALID_ROLE_SPECIFICATION
+},
+
+{
+ "spiexceptions.DiagnosticsException", "DiagnosticsException", ERRCODE_DIAGNOSTICS_EXCEPTION
+},
+
+{
+ "spiexceptions.StackedDiagnosticsAccessedWithoutActiveHandler", "StackedDiagnosticsAccessedWithoutActiveHandler", ERRCODE_STACKED_DIAGNOSTICS_ACCESSED_WITHOUT_ACTIVE_HANDLER
+},
+
+{
+ "spiexceptions.CaseNotFound", "CaseNotFound", ERRCODE_CASE_NOT_FOUND
+},
+
+{
+ "spiexceptions.CardinalityViolation", "CardinalityViolation", ERRCODE_CARDINALITY_VIOLATION
+},
+
+{
+ "spiexceptions.DataException", "DataException", ERRCODE_DATA_EXCEPTION
+},
+
+{
+ "spiexceptions.ArraySubscriptError", "ArraySubscriptError", ERRCODE_ARRAY_SUBSCRIPT_ERROR
+},
+
+{
+ "spiexceptions.CharacterNotInRepertoire", "CharacterNotInRepertoire", ERRCODE_CHARACTER_NOT_IN_REPERTOIRE
+},
+
+{
+ "spiexceptions.DatetimeFieldOverflow", "DatetimeFieldOverflow", ERRCODE_DATETIME_FIELD_OVERFLOW
+},
+
+{
+ "spiexceptions.DivisionByZero", "DivisionByZero", ERRCODE_DIVISION_BY_ZERO
+},
+
+{
+ "spiexceptions.ErrorInAssignment", "ErrorInAssignment", ERRCODE_ERROR_IN_ASSIGNMENT
+},
+
+{
+ "spiexceptions.EscapeCharacterConflict", "EscapeCharacterConflict", ERRCODE_ESCAPE_CHARACTER_CONFLICT
+},
+
+{
+ "spiexceptions.IndicatorOverflow", "IndicatorOverflow", ERRCODE_INDICATOR_OVERFLOW
+},
+
+{
+ "spiexceptions.IntervalFieldOverflow", "IntervalFieldOverflow", ERRCODE_INTERVAL_FIELD_OVERFLOW
+},
+
+{
+ "spiexceptions.InvalidArgumentForLogarithm", "InvalidArgumentForLogarithm", ERRCODE_INVALID_ARGUMENT_FOR_LOG
+},
+
+{
+ "spiexceptions.InvalidArgumentForNtileFunction", "InvalidArgumentForNtileFunction", ERRCODE_INVALID_ARGUMENT_FOR_NTILE
+},
+
+{
+ "spiexceptions.InvalidArgumentForNthValueFunction", "InvalidArgumentForNthValueFunction", ERRCODE_INVALID_ARGUMENT_FOR_NTH_VALUE
+},
+
+{
+ "spiexceptions.InvalidArgumentForPowerFunction", "InvalidArgumentForPowerFunction", ERRCODE_INVALID_ARGUMENT_FOR_POWER_FUNCTION
+},
+
+{
+ "spiexceptions.InvalidArgumentForWidthBucketFunction", "InvalidArgumentForWidthBucketFunction", ERRCODE_INVALID_ARGUMENT_FOR_WIDTH_BUCKET_FUNCTION
+},
+
+{
+ "spiexceptions.InvalidCharacterValueForCast", "InvalidCharacterValueForCast", ERRCODE_INVALID_CHARACTER_VALUE_FOR_CAST
+},
+
+{
+ "spiexceptions.InvalidDatetimeFormat", "InvalidDatetimeFormat", ERRCODE_INVALID_DATETIME_FORMAT
+},
+
+{
+ "spiexceptions.InvalidEscapeCharacter", "InvalidEscapeCharacter", ERRCODE_INVALID_ESCAPE_CHARACTER
+},
+
+{
+ "spiexceptions.InvalidEscapeOctet", "InvalidEscapeOctet", ERRCODE_INVALID_ESCAPE_OCTET
+},
+
+{
+ "spiexceptions.InvalidEscapeSequence", "InvalidEscapeSequence", ERRCODE_INVALID_ESCAPE_SEQUENCE
+},
+
+{
+ "spiexceptions.NonstandardUseOfEscapeCharacter", "NonstandardUseOfEscapeCharacter", ERRCODE_NONSTANDARD_USE_OF_ESCAPE_CHARACTER
+},
+
+{
+ "spiexceptions.InvalidIndicatorParameterValue", "InvalidIndicatorParameterValue", ERRCODE_INVALID_INDICATOR_PARAMETER_VALUE
+},
+
+{
+ "spiexceptions.InvalidParameterValue", "InvalidParameterValue", ERRCODE_INVALID_PARAMETER_VALUE
+},
+
+{
+ "spiexceptions.InvalidPrecedingOrFollowingSize", "InvalidPrecedingOrFollowingSize", ERRCODE_INVALID_PRECEDING_OR_FOLLOWING_SIZE
+},
+
+{
+ "spiexceptions.InvalidRegularExpression", "InvalidRegularExpression", ERRCODE_INVALID_REGULAR_EXPRESSION
+},
+
+{
+ "spiexceptions.InvalidRowCountInLimitClause", "InvalidRowCountInLimitClause", ERRCODE_INVALID_ROW_COUNT_IN_LIMIT_CLAUSE
+},
+
+{
+ "spiexceptions.InvalidRowCountInResultOffsetClause", "InvalidRowCountInResultOffsetClause", ERRCODE_INVALID_ROW_COUNT_IN_RESULT_OFFSET_CLAUSE
+},
+
+{
+ "spiexceptions.InvalidTablesampleArgument", "InvalidTablesampleArgument", ERRCODE_INVALID_TABLESAMPLE_ARGUMENT
+},
+
+{
+ "spiexceptions.InvalidTablesampleRepeat", "InvalidTablesampleRepeat", ERRCODE_INVALID_TABLESAMPLE_REPEAT
+},
+
+{
+ "spiexceptions.InvalidTimeZoneDisplacementValue", "InvalidTimeZoneDisplacementValue", ERRCODE_INVALID_TIME_ZONE_DISPLACEMENT_VALUE
+},
+
+{
+ "spiexceptions.InvalidUseOfEscapeCharacter", "InvalidUseOfEscapeCharacter", ERRCODE_INVALID_USE_OF_ESCAPE_CHARACTER
+},
+
+{
+ "spiexceptions.MostSpecificTypeMismatch", "MostSpecificTypeMismatch", ERRCODE_MOST_SPECIFIC_TYPE_MISMATCH
+},
+
+{
+ "spiexceptions.NullValueNotAllowed", "NullValueNotAllowed", ERRCODE_NULL_VALUE_NOT_ALLOWED
+},
+
+{
+ "spiexceptions.NullValueNoIndicatorParameter", "NullValueNoIndicatorParameter", ERRCODE_NULL_VALUE_NO_INDICATOR_PARAMETER
+},
+
+{
+ "spiexceptions.NumericValueOutOfRange", "NumericValueOutOfRange", ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE
+},
+
+{
+ "spiexceptions.SequenceGeneratorLimitExceeded", "SequenceGeneratorLimitExceeded", ERRCODE_SEQUENCE_GENERATOR_LIMIT_EXCEEDED
+},
+
+{
+ "spiexceptions.StringDataLengthMismatch", "StringDataLengthMismatch", ERRCODE_STRING_DATA_LENGTH_MISMATCH
+},
+
+{
+ "spiexceptions.StringDataRightTruncation", "StringDataRightTruncation", ERRCODE_STRING_DATA_RIGHT_TRUNCATION
+},
+
+{
+ "spiexceptions.SubstringError", "SubstringError", ERRCODE_SUBSTRING_ERROR
+},
+
+{
+ "spiexceptions.TrimError", "TrimError", ERRCODE_TRIM_ERROR
+},
+
+{
+ "spiexceptions.UnterminatedCString", "UnterminatedCString", ERRCODE_UNTERMINATED_C_STRING
+},
+
+{
+ "spiexceptions.ZeroLengthCharacterString", "ZeroLengthCharacterString", ERRCODE_ZERO_LENGTH_CHARACTER_STRING
+},
+
+{
+ "spiexceptions.FloatingPointException", "FloatingPointException", ERRCODE_FLOATING_POINT_EXCEPTION
+},
+
+{
+ "spiexceptions.InvalidTextRepresentation", "InvalidTextRepresentation", ERRCODE_INVALID_TEXT_REPRESENTATION
+},
+
+{
+ "spiexceptions.InvalidBinaryRepresentation", "InvalidBinaryRepresentation", ERRCODE_INVALID_BINARY_REPRESENTATION
+},
+
+{
+ "spiexceptions.BadCopyFileFormat", "BadCopyFileFormat", ERRCODE_BAD_COPY_FILE_FORMAT
+},
+
+{
+ "spiexceptions.UntranslatableCharacter", "UntranslatableCharacter", ERRCODE_UNTRANSLATABLE_CHARACTER
+},
+
+{
+ "spiexceptions.NotAnXmlDocument", "NotAnXmlDocument", ERRCODE_NOT_AN_XML_DOCUMENT
+},
+
+{
+ "spiexceptions.InvalidXmlDocument", "InvalidXmlDocument", ERRCODE_INVALID_XML_DOCUMENT
+},
+
+{
+ "spiexceptions.InvalidXmlContent", "InvalidXmlContent", ERRCODE_INVALID_XML_CONTENT
+},
+
+{
+ "spiexceptions.InvalidXmlComment", "InvalidXmlComment", ERRCODE_INVALID_XML_COMMENT
+},
+
+{
+ "spiexceptions.InvalidXmlProcessingInstruction", "InvalidXmlProcessingInstruction", ERRCODE_INVALID_XML_PROCESSING_INSTRUCTION
+},
+
+{
+ "spiexceptions.DuplicateJsonObjectKeyValue", "DuplicateJsonObjectKeyValue", ERRCODE_DUPLICATE_JSON_OBJECT_KEY_VALUE
+},
+
+{
+ "spiexceptions.InvalidArgumentForSqlJsonDatetimeFunction", "InvalidArgumentForSqlJsonDatetimeFunction", ERRCODE_INVALID_ARGUMENT_FOR_SQL_JSON_DATETIME_FUNCTION
+},
+
+{
+ "spiexceptions.InvalidJsonText", "InvalidJsonText", ERRCODE_INVALID_JSON_TEXT
+},
+
+{
+ "spiexceptions.InvalidSqlJsonSubscript", "InvalidSqlJsonSubscript", ERRCODE_INVALID_SQL_JSON_SUBSCRIPT
+},
+
+{
+ "spiexceptions.MoreThanOneSqlJsonItem", "MoreThanOneSqlJsonItem", ERRCODE_MORE_THAN_ONE_SQL_JSON_ITEM
+},
+
+{
+ "spiexceptions.NoSqlJsonItem", "NoSqlJsonItem", ERRCODE_NO_SQL_JSON_ITEM
+},
+
+{
+ "spiexceptions.NonNumericSqlJsonItem", "NonNumericSqlJsonItem", ERRCODE_NON_NUMERIC_SQL_JSON_ITEM
+},
+
+{
+ "spiexceptions.NonUniqueKeysInAJsonObject", "NonUniqueKeysInAJsonObject", ERRCODE_NON_UNIQUE_KEYS_IN_A_JSON_OBJECT
+},
+
+{
+ "spiexceptions.SingletonSqlJsonItemRequired", "SingletonSqlJsonItemRequired", ERRCODE_SINGLETON_SQL_JSON_ITEM_REQUIRED
+},
+
+{
+ "spiexceptions.SqlJsonArrayNotFound", "SqlJsonArrayNotFound", ERRCODE_SQL_JSON_ARRAY_NOT_FOUND
+},
+
+{
+ "spiexceptions.SqlJsonMemberNotFound", "SqlJsonMemberNotFound", ERRCODE_SQL_JSON_MEMBER_NOT_FOUND
+},
+
+{
+ "spiexceptions.SqlJsonNumberNotFound", "SqlJsonNumberNotFound", ERRCODE_SQL_JSON_NUMBER_NOT_FOUND
+},
+
+{
+ "spiexceptions.SqlJsonObjectNotFound", "SqlJsonObjectNotFound", ERRCODE_SQL_JSON_OBJECT_NOT_FOUND
+},
+
+{
+ "spiexceptions.TooManyJsonArrayElements", "TooManyJsonArrayElements", ERRCODE_TOO_MANY_JSON_ARRAY_ELEMENTS
+},
+
+{
+ "spiexceptions.TooManyJsonObjectMembers", "TooManyJsonObjectMembers", ERRCODE_TOO_MANY_JSON_OBJECT_MEMBERS
+},
+
+{
+ "spiexceptions.SqlJsonScalarRequired", "SqlJsonScalarRequired", ERRCODE_SQL_JSON_SCALAR_REQUIRED
+},
+
+{
+ "spiexceptions.SqlJsonItemCannotBeCastToTargetType", "SqlJsonItemCannotBeCastToTargetType", ERRCODE_SQL_JSON_ITEM_CANNOT_BE_CAST_TO_TARGET_TYPE
+},
+
+{
+ "spiexceptions.IntegrityConstraintViolation", "IntegrityConstraintViolation", ERRCODE_INTEGRITY_CONSTRAINT_VIOLATION
+},
+
+{
+ "spiexceptions.RestrictViolation", "RestrictViolation", ERRCODE_RESTRICT_VIOLATION
+},
+
+{
+ "spiexceptions.NotNullViolation", "NotNullViolation", ERRCODE_NOT_NULL_VIOLATION
+},
+
+{
+ "spiexceptions.ForeignKeyViolation", "ForeignKeyViolation", ERRCODE_FOREIGN_KEY_VIOLATION
+},
+
+{
+ "spiexceptions.UniqueViolation", "UniqueViolation", ERRCODE_UNIQUE_VIOLATION
+},
+
+{
+ "spiexceptions.CheckViolation", "CheckViolation", ERRCODE_CHECK_VIOLATION
+},
+
+{
+ "spiexceptions.ExclusionViolation", "ExclusionViolation", ERRCODE_EXCLUSION_VIOLATION
+},
+
+{
+ "spiexceptions.InvalidCursorState", "InvalidCursorState", ERRCODE_INVALID_CURSOR_STATE
+},
+
+{
+ "spiexceptions.InvalidTransactionState", "InvalidTransactionState", ERRCODE_INVALID_TRANSACTION_STATE
+},
+
+{
+ "spiexceptions.ActiveSqlTransaction", "ActiveSqlTransaction", ERRCODE_ACTIVE_SQL_TRANSACTION
+},
+
+{
+ "spiexceptions.BranchTransactionAlreadyActive", "BranchTransactionAlreadyActive", ERRCODE_BRANCH_TRANSACTION_ALREADY_ACTIVE
+},
+
+{
+ "spiexceptions.HeldCursorRequiresSameIsolationLevel", "HeldCursorRequiresSameIsolationLevel", ERRCODE_HELD_CURSOR_REQUIRES_SAME_ISOLATION_LEVEL
+},
+
+{
+ "spiexceptions.InappropriateAccessModeForBranchTransaction", "InappropriateAccessModeForBranchTransaction", ERRCODE_INAPPROPRIATE_ACCESS_MODE_FOR_BRANCH_TRANSACTION
+},
+
+{
+ "spiexceptions.InappropriateIsolationLevelForBranchTransaction", "InappropriateIsolationLevelForBranchTransaction", ERRCODE_INAPPROPRIATE_ISOLATION_LEVEL_FOR_BRANCH_TRANSACTION
+},
+
+{
+ "spiexceptions.NoActiveSqlTransactionForBranchTransaction", "NoActiveSqlTransactionForBranchTransaction", ERRCODE_NO_ACTIVE_SQL_TRANSACTION_FOR_BRANCH_TRANSACTION
+},
+
+{
+ "spiexceptions.ReadOnlySqlTransaction", "ReadOnlySqlTransaction", ERRCODE_READ_ONLY_SQL_TRANSACTION
+},
+
+{
+ "spiexceptions.SchemaAndDataStatementMixingNotSupported", "SchemaAndDataStatementMixingNotSupported", ERRCODE_SCHEMA_AND_DATA_STATEMENT_MIXING_NOT_SUPPORTED
+},
+
+{
+ "spiexceptions.NoActiveSqlTransaction", "NoActiveSqlTransaction", ERRCODE_NO_ACTIVE_SQL_TRANSACTION
+},
+
+{
+ "spiexceptions.InFailedSqlTransaction", "InFailedSqlTransaction", ERRCODE_IN_FAILED_SQL_TRANSACTION
+},
+
+{
+ "spiexceptions.IdleInTransactionSessionTimeout", "IdleInTransactionSessionTimeout", ERRCODE_IDLE_IN_TRANSACTION_SESSION_TIMEOUT
+},
+
+{
+ "spiexceptions.InvalidSqlStatementName", "InvalidSqlStatementName", ERRCODE_INVALID_SQL_STATEMENT_NAME
+},
+
+{
+ "spiexceptions.TriggeredDataChangeViolation", "TriggeredDataChangeViolation", ERRCODE_TRIGGERED_DATA_CHANGE_VIOLATION
+},
+
+{
+ "spiexceptions.InvalidAuthorizationSpecification", "InvalidAuthorizationSpecification", ERRCODE_INVALID_AUTHORIZATION_SPECIFICATION
+},
+
+{
+ "spiexceptions.InvalidPassword", "InvalidPassword", ERRCODE_INVALID_PASSWORD
+},
+
+{
+ "spiexceptions.DependentPrivilegeDescriptorsStillExist", "DependentPrivilegeDescriptorsStillExist", ERRCODE_DEPENDENT_PRIVILEGE_DESCRIPTORS_STILL_EXIST
+},
+
+{
+ "spiexceptions.DependentObjectsStillExist", "DependentObjectsStillExist", ERRCODE_DEPENDENT_OBJECTS_STILL_EXIST
+},
+
+{
+ "spiexceptions.InvalidTransactionTermination", "InvalidTransactionTermination", ERRCODE_INVALID_TRANSACTION_TERMINATION
+},
+
+{
+ "spiexceptions.SqlRoutineException", "SqlRoutineException", ERRCODE_SQL_ROUTINE_EXCEPTION
+},
+
+{
+ "spiexceptions.FunctionExecutedNoReturnStatement", "FunctionExecutedNoReturnStatement", ERRCODE_S_R_E_FUNCTION_EXECUTED_NO_RETURN_STATEMENT
+},
+
+{
+ "spiexceptions.ModifyingSqlDataNotPermitted", "ModifyingSqlDataNotPermitted", ERRCODE_S_R_E_MODIFYING_SQL_DATA_NOT_PERMITTED
+},
+
+{
+ "spiexceptions.ProhibitedSqlStatementAttempted", "ProhibitedSqlStatementAttempted", ERRCODE_S_R_E_PROHIBITED_SQL_STATEMENT_ATTEMPTED
+},
+
+{
+ "spiexceptions.ReadingSqlDataNotPermitted", "ReadingSqlDataNotPermitted", ERRCODE_S_R_E_READING_SQL_DATA_NOT_PERMITTED
+},
+
+{
+ "spiexceptions.InvalidCursorName", "InvalidCursorName", ERRCODE_INVALID_CURSOR_NAME
+},
+
+{
+ "spiexceptions.ExternalRoutineException", "ExternalRoutineException", ERRCODE_EXTERNAL_ROUTINE_EXCEPTION
+},
+
+{
+ "spiexceptions.ContainingSqlNotPermitted", "ContainingSqlNotPermitted", ERRCODE_E_R_E_CONTAINING_SQL_NOT_PERMITTED
+},
+
+{
+ "spiexceptions.ModifyingSqlDataNotPermitted", "ModifyingSqlDataNotPermitted", ERRCODE_E_R_E_MODIFYING_SQL_DATA_NOT_PERMITTED
+},
+
+{
+ "spiexceptions.ProhibitedSqlStatementAttempted", "ProhibitedSqlStatementAttempted", ERRCODE_E_R_E_PROHIBITED_SQL_STATEMENT_ATTEMPTED
+},
+
+{
+ "spiexceptions.ReadingSqlDataNotPermitted", "ReadingSqlDataNotPermitted", ERRCODE_E_R_E_READING_SQL_DATA_NOT_PERMITTED
+},
+
+{
+ "spiexceptions.ExternalRoutineInvocationException", "ExternalRoutineInvocationException", ERRCODE_EXTERNAL_ROUTINE_INVOCATION_EXCEPTION
+},
+
+{
+ "spiexceptions.InvalidSqlstateReturned", "InvalidSqlstateReturned", ERRCODE_E_R_I_E_INVALID_SQLSTATE_RETURNED
+},
+
+{
+ "spiexceptions.NullValueNotAllowed", "NullValueNotAllowed", ERRCODE_E_R_I_E_NULL_VALUE_NOT_ALLOWED
+},
+
+{
+ "spiexceptions.TriggerProtocolViolated", "TriggerProtocolViolated", ERRCODE_E_R_I_E_TRIGGER_PROTOCOL_VIOLATED
+},
+
+{
+ "spiexceptions.SrfProtocolViolated", "SrfProtocolViolated", ERRCODE_E_R_I_E_SRF_PROTOCOL_VIOLATED
+},
+
+{
+ "spiexceptions.EventTriggerProtocolViolated", "EventTriggerProtocolViolated", ERRCODE_E_R_I_E_EVENT_TRIGGER_PROTOCOL_VIOLATED
+},
+
+{
+ "spiexceptions.SavepointException", "SavepointException", ERRCODE_SAVEPOINT_EXCEPTION
+},
+
+{
+ "spiexceptions.InvalidSavepointSpecification", "InvalidSavepointSpecification", ERRCODE_S_E_INVALID_SPECIFICATION
+},
+
+{
+ "spiexceptions.InvalidCatalogName", "InvalidCatalogName", ERRCODE_INVALID_CATALOG_NAME
+},
+
+{
+ "spiexceptions.InvalidSchemaName", "InvalidSchemaName", ERRCODE_INVALID_SCHEMA_NAME
+},
+
+{
+ "spiexceptions.TransactionRollback", "TransactionRollback", ERRCODE_TRANSACTION_ROLLBACK
+},
+
+{
+ "spiexceptions.TransactionIntegrityConstraintViolation", "TransactionIntegrityConstraintViolation", ERRCODE_T_R_INTEGRITY_CONSTRAINT_VIOLATION
+},
+
+{
+ "spiexceptions.SerializationFailure", "SerializationFailure", ERRCODE_T_R_SERIALIZATION_FAILURE
+},
+
+{
+ "spiexceptions.StatementCompletionUnknown", "StatementCompletionUnknown", ERRCODE_T_R_STATEMENT_COMPLETION_UNKNOWN
+},
+
+{
+ "spiexceptions.DeadlockDetected", "DeadlockDetected", ERRCODE_T_R_DEADLOCK_DETECTED
+},
+
+{
+ "spiexceptions.SyntaxErrorOrAccessRuleViolation", "SyntaxErrorOrAccessRuleViolation", ERRCODE_SYNTAX_ERROR_OR_ACCESS_RULE_VIOLATION
+},
+
+{
+ "spiexceptions.SyntaxError", "SyntaxError", ERRCODE_SYNTAX_ERROR
+},
+
+{
+ "spiexceptions.InsufficientPrivilege", "InsufficientPrivilege", ERRCODE_INSUFFICIENT_PRIVILEGE
+},
+
+{
+ "spiexceptions.CannotCoerce", "CannotCoerce", ERRCODE_CANNOT_COERCE
+},
+
+{
+ "spiexceptions.GroupingError", "GroupingError", ERRCODE_GROUPING_ERROR
+},
+
+{
+ "spiexceptions.WindowingError", "WindowingError", ERRCODE_WINDOWING_ERROR
+},
+
+{
+ "spiexceptions.InvalidRecursion", "InvalidRecursion", ERRCODE_INVALID_RECURSION
+},
+
+{
+ "spiexceptions.InvalidForeignKey", "InvalidForeignKey", ERRCODE_INVALID_FOREIGN_KEY
+},
+
+{
+ "spiexceptions.InvalidName", "InvalidName", ERRCODE_INVALID_NAME
+},
+
+{
+ "spiexceptions.NameTooLong", "NameTooLong", ERRCODE_NAME_TOO_LONG
+},
+
+{
+ "spiexceptions.ReservedName", "ReservedName", ERRCODE_RESERVED_NAME
+},
+
+{
+ "spiexceptions.DatatypeMismatch", "DatatypeMismatch", ERRCODE_DATATYPE_MISMATCH
+},
+
+{
+ "spiexceptions.IndeterminateDatatype", "IndeterminateDatatype", ERRCODE_INDETERMINATE_DATATYPE
+},
+
+{
+ "spiexceptions.CollationMismatch", "CollationMismatch", ERRCODE_COLLATION_MISMATCH
+},
+
+{
+ "spiexceptions.IndeterminateCollation", "IndeterminateCollation", ERRCODE_INDETERMINATE_COLLATION
+},
+
+{
+ "spiexceptions.WrongObjectType", "WrongObjectType", ERRCODE_WRONG_OBJECT_TYPE
+},
+
+{
+ "spiexceptions.GeneratedAlways", "GeneratedAlways", ERRCODE_GENERATED_ALWAYS
+},
+
+{
+ "spiexceptions.UndefinedColumn", "UndefinedColumn", ERRCODE_UNDEFINED_COLUMN
+},
+
+{
+ "spiexceptions.UndefinedFunction", "UndefinedFunction", ERRCODE_UNDEFINED_FUNCTION
+},
+
+{
+ "spiexceptions.UndefinedTable", "UndefinedTable", ERRCODE_UNDEFINED_TABLE
+},
+
+{
+ "spiexceptions.UndefinedParameter", "UndefinedParameter", ERRCODE_UNDEFINED_PARAMETER
+},
+
+{
+ "spiexceptions.UndefinedObject", "UndefinedObject", ERRCODE_UNDEFINED_OBJECT
+},
+
+{
+ "spiexceptions.DuplicateColumn", "DuplicateColumn", ERRCODE_DUPLICATE_COLUMN
+},
+
+{
+ "spiexceptions.DuplicateCursor", "DuplicateCursor", ERRCODE_DUPLICATE_CURSOR
+},
+
+{
+ "spiexceptions.DuplicateDatabase", "DuplicateDatabase", ERRCODE_DUPLICATE_DATABASE
+},
+
+{
+ "spiexceptions.DuplicateFunction", "DuplicateFunction", ERRCODE_DUPLICATE_FUNCTION
+},
+
+{
+ "spiexceptions.DuplicatePreparedStatement", "DuplicatePreparedStatement", ERRCODE_DUPLICATE_PSTATEMENT
+},
+
+{
+ "spiexceptions.DuplicateSchema", "DuplicateSchema", ERRCODE_DUPLICATE_SCHEMA
+},
+
+{
+ "spiexceptions.DuplicateTable", "DuplicateTable", ERRCODE_DUPLICATE_TABLE
+},
+
+{
+ "spiexceptions.DuplicateAlias", "DuplicateAlias", ERRCODE_DUPLICATE_ALIAS
+},
+
+{
+ "spiexceptions.DuplicateObject", "DuplicateObject", ERRCODE_DUPLICATE_OBJECT
+},
+
+{
+ "spiexceptions.AmbiguousColumn", "AmbiguousColumn", ERRCODE_AMBIGUOUS_COLUMN
+},
+
+{
+ "spiexceptions.AmbiguousFunction", "AmbiguousFunction", ERRCODE_AMBIGUOUS_FUNCTION
+},
+
+{
+ "spiexceptions.AmbiguousParameter", "AmbiguousParameter", ERRCODE_AMBIGUOUS_PARAMETER
+},
+
+{
+ "spiexceptions.AmbiguousAlias", "AmbiguousAlias", ERRCODE_AMBIGUOUS_ALIAS
+},
+
+{
+ "spiexceptions.InvalidColumnReference", "InvalidColumnReference", ERRCODE_INVALID_COLUMN_REFERENCE
+},
+
+{
+ "spiexceptions.InvalidColumnDefinition", "InvalidColumnDefinition", ERRCODE_INVALID_COLUMN_DEFINITION
+},
+
+{
+ "spiexceptions.InvalidCursorDefinition", "InvalidCursorDefinition", ERRCODE_INVALID_CURSOR_DEFINITION
+},
+
+{
+ "spiexceptions.InvalidDatabaseDefinition", "InvalidDatabaseDefinition", ERRCODE_INVALID_DATABASE_DEFINITION
+},
+
+{
+ "spiexceptions.InvalidFunctionDefinition", "InvalidFunctionDefinition", ERRCODE_INVALID_FUNCTION_DEFINITION
+},
+
+{
+ "spiexceptions.InvalidPreparedStatementDefinition", "InvalidPreparedStatementDefinition", ERRCODE_INVALID_PSTATEMENT_DEFINITION
+},
+
+{
+ "spiexceptions.InvalidSchemaDefinition", "InvalidSchemaDefinition", ERRCODE_INVALID_SCHEMA_DEFINITION
+},
+
+{
+ "spiexceptions.InvalidTableDefinition", "InvalidTableDefinition", ERRCODE_INVALID_TABLE_DEFINITION
+},
+
+{
+ "spiexceptions.InvalidObjectDefinition", "InvalidObjectDefinition", ERRCODE_INVALID_OBJECT_DEFINITION
+},
+
+{
+ "spiexceptions.WithCheckOptionViolation", "WithCheckOptionViolation", ERRCODE_WITH_CHECK_OPTION_VIOLATION
+},
+
+{
+ "spiexceptions.InsufficientResources", "InsufficientResources", ERRCODE_INSUFFICIENT_RESOURCES
+},
+
+{
+ "spiexceptions.DiskFull", "DiskFull", ERRCODE_DISK_FULL
+},
+
+{
+ "spiexceptions.OutOfMemory", "OutOfMemory", ERRCODE_OUT_OF_MEMORY
+},
+
+{
+ "spiexceptions.TooManyConnections", "TooManyConnections", ERRCODE_TOO_MANY_CONNECTIONS
+},
+
+{
+ "spiexceptions.ConfigurationLimitExceeded", "ConfigurationLimitExceeded", ERRCODE_CONFIGURATION_LIMIT_EXCEEDED
+},
+
+{
+ "spiexceptions.ProgramLimitExceeded", "ProgramLimitExceeded", ERRCODE_PROGRAM_LIMIT_EXCEEDED
+},
+
+{
+ "spiexceptions.StatementTooComplex", "StatementTooComplex", ERRCODE_STATEMENT_TOO_COMPLEX
+},
+
+{
+ "spiexceptions.TooManyColumns", "TooManyColumns", ERRCODE_TOO_MANY_COLUMNS
+},
+
+{
+ "spiexceptions.TooManyArguments", "TooManyArguments", ERRCODE_TOO_MANY_ARGUMENTS
+},
+
+{
+ "spiexceptions.ObjectNotInPrerequisiteState", "ObjectNotInPrerequisiteState", ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE
+},
+
+{
+ "spiexceptions.ObjectInUse", "ObjectInUse", ERRCODE_OBJECT_IN_USE
+},
+
+{
+ "spiexceptions.CantChangeRuntimeParam", "CantChangeRuntimeParam", ERRCODE_CANT_CHANGE_RUNTIME_PARAM
+},
+
+{
+ "spiexceptions.LockNotAvailable", "LockNotAvailable", ERRCODE_LOCK_NOT_AVAILABLE
+},
+
+{
+ "spiexceptions.UnsafeNewEnumValueUsage", "UnsafeNewEnumValueUsage", ERRCODE_UNSAFE_NEW_ENUM_VALUE_USAGE
+},
+
+{
+ "spiexceptions.OperatorIntervention", "OperatorIntervention", ERRCODE_OPERATOR_INTERVENTION
+},
+
+{
+ "spiexceptions.QueryCanceled", "QueryCanceled", ERRCODE_QUERY_CANCELED
+},
+
+{
+ "spiexceptions.AdminShutdown", "AdminShutdown", ERRCODE_ADMIN_SHUTDOWN
+},
+
+{
+ "spiexceptions.CrashShutdown", "CrashShutdown", ERRCODE_CRASH_SHUTDOWN
+},
+
+{
+ "spiexceptions.CannotConnectNow", "CannotConnectNow", ERRCODE_CANNOT_CONNECT_NOW
+},
+
+{
+ "spiexceptions.DatabaseDropped", "DatabaseDropped", ERRCODE_DATABASE_DROPPED
+},
+
+{
+ "spiexceptions.IdleSessionTimeout", "IdleSessionTimeout", ERRCODE_IDLE_SESSION_TIMEOUT
+},
+
+{
+ "spiexceptions.SystemError", "SystemError", ERRCODE_SYSTEM_ERROR
+},
+
+{
+ "spiexceptions.IoError", "IoError", ERRCODE_IO_ERROR
+},
+
+{
+ "spiexceptions.UndefinedFile", "UndefinedFile", ERRCODE_UNDEFINED_FILE
+},
+
+{
+ "spiexceptions.DuplicateFile", "DuplicateFile", ERRCODE_DUPLICATE_FILE
+},
+
+{
+ "spiexceptions.SnapshotTooOld", "SnapshotTooOld", ERRCODE_SNAPSHOT_TOO_OLD
+},
+
+{
+ "spiexceptions.ConfigFileError", "ConfigFileError", ERRCODE_CONFIG_FILE_ERROR
+},
+
+{
+ "spiexceptions.LockFileExists", "LockFileExists", ERRCODE_LOCK_FILE_EXISTS
+},
+
+{
+ "spiexceptions.FdwError", "FdwError", ERRCODE_FDW_ERROR
+},
+
+{
+ "spiexceptions.FdwColumnNameNotFound", "FdwColumnNameNotFound", ERRCODE_FDW_COLUMN_NAME_NOT_FOUND
+},
+
+{
+ "spiexceptions.FdwDynamicParameterValueNeeded", "FdwDynamicParameterValueNeeded", ERRCODE_FDW_DYNAMIC_PARAMETER_VALUE_NEEDED
+},
+
+{
+ "spiexceptions.FdwFunctionSequenceError", "FdwFunctionSequenceError", ERRCODE_FDW_FUNCTION_SEQUENCE_ERROR
+},
+
+{
+ "spiexceptions.FdwInconsistentDescriptorInformation", "FdwInconsistentDescriptorInformation", ERRCODE_FDW_INCONSISTENT_DESCRIPTOR_INFORMATION
+},
+
+{
+ "spiexceptions.FdwInvalidAttributeValue", "FdwInvalidAttributeValue", ERRCODE_FDW_INVALID_ATTRIBUTE_VALUE
+},
+
+{
+ "spiexceptions.FdwInvalidColumnName", "FdwInvalidColumnName", ERRCODE_FDW_INVALID_COLUMN_NAME
+},
+
+{
+ "spiexceptions.FdwInvalidColumnNumber", "FdwInvalidColumnNumber", ERRCODE_FDW_INVALID_COLUMN_NUMBER
+},
+
+{
+ "spiexceptions.FdwInvalidDataType", "FdwInvalidDataType", ERRCODE_FDW_INVALID_DATA_TYPE
+},
+
+{
+ "spiexceptions.FdwInvalidDataTypeDescriptors", "FdwInvalidDataTypeDescriptors", ERRCODE_FDW_INVALID_DATA_TYPE_DESCRIPTORS
+},
+
+{
+ "spiexceptions.FdwInvalidDescriptorFieldIdentifier", "FdwInvalidDescriptorFieldIdentifier", ERRCODE_FDW_INVALID_DESCRIPTOR_FIELD_IDENTIFIER
+},
+
+{
+ "spiexceptions.FdwInvalidHandle", "FdwInvalidHandle", ERRCODE_FDW_INVALID_HANDLE
+},
+
+{
+ "spiexceptions.FdwInvalidOptionIndex", "FdwInvalidOptionIndex", ERRCODE_FDW_INVALID_OPTION_INDEX
+},
+
+{
+ "spiexceptions.FdwInvalidOptionName", "FdwInvalidOptionName", ERRCODE_FDW_INVALID_OPTION_NAME
+},
+
+{
+ "spiexceptions.FdwInvalidStringLengthOrBufferLength", "FdwInvalidStringLengthOrBufferLength", ERRCODE_FDW_INVALID_STRING_LENGTH_OR_BUFFER_LENGTH
+},
+
+{
+ "spiexceptions.FdwInvalidStringFormat", "FdwInvalidStringFormat", ERRCODE_FDW_INVALID_STRING_FORMAT
+},
+
+{
+ "spiexceptions.FdwInvalidUseOfNullPointer", "FdwInvalidUseOfNullPointer", ERRCODE_FDW_INVALID_USE_OF_NULL_POINTER
+},
+
+{
+ "spiexceptions.FdwTooManyHandles", "FdwTooManyHandles", ERRCODE_FDW_TOO_MANY_HANDLES
+},
+
+{
+ "spiexceptions.FdwOutOfMemory", "FdwOutOfMemory", ERRCODE_FDW_OUT_OF_MEMORY
+},
+
+{
+ "spiexceptions.FdwNoSchemas", "FdwNoSchemas", ERRCODE_FDW_NO_SCHEMAS
+},
+
+{
+ "spiexceptions.FdwOptionNameNotFound", "FdwOptionNameNotFound", ERRCODE_FDW_OPTION_NAME_NOT_FOUND
+},
+
+{
+ "spiexceptions.FdwReplyHandle", "FdwReplyHandle", ERRCODE_FDW_REPLY_HANDLE
+},
+
+{
+ "spiexceptions.FdwSchemaNotFound", "FdwSchemaNotFound", ERRCODE_FDW_SCHEMA_NOT_FOUND
+},
+
+{
+ "spiexceptions.FdwTableNotFound", "FdwTableNotFound", ERRCODE_FDW_TABLE_NOT_FOUND
+},
+
+{
+ "spiexceptions.FdwUnableToCreateExecution", "FdwUnableToCreateExecution", ERRCODE_FDW_UNABLE_TO_CREATE_EXECUTION
+},
+
+{
+ "spiexceptions.FdwUnableToCreateReply", "FdwUnableToCreateReply", ERRCODE_FDW_UNABLE_TO_CREATE_REPLY
+},
+
+{
+ "spiexceptions.FdwUnableToEstablishConnection", "FdwUnableToEstablishConnection", ERRCODE_FDW_UNABLE_TO_ESTABLISH_CONNECTION
+},
+
+{
+ "spiexceptions.PlpgsqlError", "PlpgsqlError", ERRCODE_PLPGSQL_ERROR
+},
+
+{
+ "spiexceptions.RaiseException", "RaiseException", ERRCODE_RAISE_EXCEPTION
+},
+
+{
+ "spiexceptions.NoDataFound", "NoDataFound", ERRCODE_NO_DATA_FOUND
+},
+
+{
+ "spiexceptions.TooManyRows", "TooManyRows", ERRCODE_TOO_MANY_ROWS
+},
+
+{
+ "spiexceptions.AssertFailure", "AssertFailure", ERRCODE_ASSERT_FAILURE
+},
+
+{
+ "spiexceptions.InternalError", "InternalError", ERRCODE_INTERNAL_ERROR
+},
+
+{
+ "spiexceptions.DataCorrupted", "DataCorrupted", ERRCODE_DATA_CORRUPTED
+},
+
+{
+ "spiexceptions.IndexCorrupted", "IndexCorrupted", ERRCODE_INDEX_CORRUPTED
+},
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..13d6ce7
--- /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 plpgsql
+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..9bb1c0b
--- /dev/null
+++ b/src/pl/plpython/sql/plpython_error.sql
@@ -0,0 +1,357 @@
+-- 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();
+
+/* test error logged with an underlying exception that includes a detail
+ * string (bug #18070).
+ */
+CREATE FUNCTION python_error_detail() RETURNS SETOF text AS $$
+ plan = plpy.prepare("SELECT to_date('xy', 'DD') d")
+ for row in plpy.cursor(plan):
+ yield row['d']
+$$ LANGUAGE plpython3u;
+
+SELECT python_error_detail();
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";