[PATCH] Add transforms feature

Поиск
Список
Период
Сортировка
От Peter Eisentraut
Тема [PATCH] Add transforms feature
Дата
Msg-id 1371179501.15555.12.camel@vanquo.pezone.net
обсуждение исходный текст
Ответ на Re: transforms  (Peter Eisentraut <peter_e@gmx.net>)
Ответы Re: [PATCH] Add transforms feature  (Cédric Villemain <cedric@2ndquadrant.com>)
Re: [PATCH] Add transforms feature  (Craig Ringer <craig@2ndquadrant.com>)
Re: [PATCH] Add transforms feature  (Alvaro Herrera <alvherre@2ndquadrant.com>)
Re: [PATCH] Add transforms feature  (Hitoshi Harada <umi.tanuki@gmail.com>)
Список pgsql-hackers
A transform is an SQL object that supplies to functions for converting
between data types and procedural languages.  For example, a transform
could arrange that hstore is converted to an appropriate hash or
dictionary object in PL/Perl or PL/Python.

Externally visible changes:

- new SQL commands CREATE TRANSFORM and DROP TRANSFORM

- system catalog pg_transform

- transform support in PL/Perl and PL/Python

- PL/Perl and PL/Python install their header files for use by external
  types

- transforms contrib modules hstore_plperl, hstore_plpython,
  ltree_plpython

- regression test mangling for Python 3 was moved to a separate
  makefile, for use by extensions

The regression tests for general CREATE TRANSFORM functionality are
under the hstore_plperl module in order to be able to test it on a real
transform implementation, instead of having to create an entire fake
procedural language under src/test/regress/, say.

Dynamic module linking on OS X was changed to allow undefined symbols at
build time.  This is necessary so that a transform module can use
symbols from the type and the language modules, if necessary.  Other
platforms already behaved this way, but the default on OS X was
different.
---
Continued from 2013-01 commit fest.  All known open issues have been fixed.

 contrib/Makefile                                   |   12 +
 contrib/hstore_plperl/.gitignore                   |    4 +
 contrib/hstore_plperl/Makefile                     |   23 ++
 .../hstore_plperl/expected/create_transform.out    |   70 +++++
 contrib/hstore_plperl/expected/hstore_plperl.out   |  170 ++++++++++++
 contrib/hstore_plperl/hstore_plperl--1.0.sql       |   17 ++
 contrib/hstore_plperl/hstore_plperl.c              |   90 ++++++
 contrib/hstore_plperl/hstore_plperl.control        |    6 +
 contrib/hstore_plperl/hstore_plperlu--1.0.sql      |   17 ++
 contrib/hstore_plperl/hstore_plperlu.control       |    6 +
 contrib/hstore_plperl/sql/create_transform.sql     |   45 +++
 contrib/hstore_plperl/sql/hstore_plperl.sql        |  120 ++++++++
 contrib/hstore_plpython/.gitignore                 |    4 +
 contrib/hstore_plpython/Makefile                   |   30 ++
 .../hstore_plpython/expected/hstore_plpython.out   |  137 +++++++++
 contrib/hstore_plpython/hstore_plpython.c          |  116 ++++++++
 contrib/hstore_plpython/hstore_plpython2u--1.0.sql |   19 ++
 contrib/hstore_plpython/hstore_plpython2u.control  |    6 +
 contrib/hstore_plpython/hstore_plpython3u--1.0.sql |   19 ++
 contrib/hstore_plpython/hstore_plpython3u.control  |    6 +
 contrib/hstore_plpython/hstore_plpythonu--1.0.sql  |   19 ++
 contrib/hstore_plpython/hstore_plpythonu.control   |    6 +
 contrib/hstore_plpython/sql/hstore_plpython.sql    |  106 +++++++
 contrib/ltree_plpython/.gitignore                  |    4 +
 contrib/ltree_plpython/Makefile                    |   30 ++
 contrib/ltree_plpython/expected/ltree_plpython.out |   42 +++
 contrib/ltree_plpython/ltree_plpython.c            |   32 +++
 contrib/ltree_plpython/ltree_plpython2u--1.0.sql   |   12 +
 contrib/ltree_plpython/ltree_plpython2u.control    |    6 +
 contrib/ltree_plpython/ltree_plpython3u--1.0.sql   |   12 +
 contrib/ltree_plpython/ltree_plpython3u.control    |    6 +
 contrib/ltree_plpython/ltree_plpythonu--1.0.sql    |   12 +
 contrib/ltree_plpython/ltree_plpythonu.control     |    6 +
 contrib/ltree_plpython/sql/ltree_plpython.sql      |   34 +++
 doc/src/sgml/catalogs.sgml                         |   73 +++++
 doc/src/sgml/hstore.sgml                           |   18 ++
 doc/src/sgml/ltree.sgml                            |   15 +
 doc/src/sgml/ref/allfiles.sgml                     |    2 +
 doc/src/sgml/ref/alter_extension.sgml              |   21 ++
 doc/src/sgml/ref/comment.sgml                      |   22 ++
 doc/src/sgml/ref/create_transform.sgml             |  187 +++++++++++++
 doc/src/sgml/ref/drop_transform.sgml               |  123 ++++++++
 doc/src/sgml/reference.sgml                        |    2 +
 src/Makefile.shlib                                 |    2 +-
 src/backend/catalog/Makefile                       |    1 +
 src/backend/catalog/dependency.c                   |    8 +
 src/backend/catalog/objectaddress.c                |   76 ++++-
 src/backend/catalog/pg_proc.c                      |   20 ++
 src/backend/commands/dropcmds.c                    |    6 +
 src/backend/commands/event_trigger.c               |    3 +
 src/backend/commands/functioncmds.c                |  293 ++++++++++++++++++++
 src/backend/nodes/copyfuncs.c                      |   17 ++
 src/backend/nodes/equalfuncs.c                     |   15 +
 src/backend/parser/gram.y                          |   82 +++++-
 src/backend/parser/parse_func.c                    |    5 +
 src/backend/tcop/utility.c                         |   16 ++
 src/backend/utils/adt/ruleutils.c                  |   11 +-
 src/backend/utils/cache/lsyscache.c                |   83 ++++++
 src/backend/utils/cache/syscache.c                 |   23 ++
 src/bin/pg_dump/common.c                           |    5 +
 src/bin/pg_dump/pg_dump.c                          |  230 +++++++++++++++
 src/bin/pg_dump/pg_dump.h                          |   11 +
 src/bin/pg_dump/pg_dump_sort.c                     |   13 +-
 src/include/catalog/catversion.h                   |    2 +-
 src/include/catalog/dependency.h                   |    1 +
 src/include/catalog/indexing.h                     |    5 +
 src/include/catalog/pg_transform.h                 |   47 ++++
 src/include/commands/defrem.h                      |    3 +
 src/include/nodes/nodes.h                          |    1 +
 src/include/nodes/parsenodes.h                     |   15 +
 src/include/parser/kwlist.h                        |    2 +
 src/include/utils/lsyscache.h                      |    4 +
 src/include/utils/syscache.h                       |    2 +
 src/interfaces/ecpg/preproc/ecpg.tokens            |    2 +-
 src/interfaces/ecpg/preproc/ecpg.trailer           |    5 +-
 src/interfaces/ecpg/preproc/ecpg_keywords.c        |    2 -
 src/pl/plperl/GNUmakefile                          |    4 +-
 src/pl/plperl/plperl.c                             |   32 ++-
 src/pl/plperl/plperl_helpers.h                     |    2 +
 src/pl/plpython/Makefile                           |   40 +--
 src/pl/plpython/plpy_main.c                        |    1 +
 src/pl/plpython/plpy_procedure.c                   |    6 +-
 src/pl/plpython/plpy_procedure.h                   |    1 +
 src/pl/plpython/plpy_spi.c                         |    3 +-
 src/pl/plpython/plpy_typeio.c                      |  158 +++++++----
 src/pl/plpython/plpy_typeio.h                      |    9 +-
 src/pl/plpython/plpy_util.c                        |   21 +-
 src/pl/plpython/plpy_util.h                        |    1 +
 src/pl/plpython/plpython.h                         |    1 +
 src/pl/plpython/regress-python3-mangle.mk          |   35 +++
 src/test/regress/expected/sanity_check.out         |    3 +-
 91 files changed, 2891 insertions(+), 144 deletions(-)
 create mode 100644 contrib/hstore_plperl/.gitignore
 create mode 100644 contrib/hstore_plperl/Makefile
 create mode 100644 contrib/hstore_plperl/expected/create_transform.out
 create mode 100644 contrib/hstore_plperl/expected/hstore_plperl.out
 create mode 100644 contrib/hstore_plperl/hstore_plperl--1.0.sql
 create mode 100644 contrib/hstore_plperl/hstore_plperl.c
 create mode 100644 contrib/hstore_plperl/hstore_plperl.control
 create mode 100644 contrib/hstore_plperl/hstore_plperlu--1.0.sql
 create mode 100644 contrib/hstore_plperl/hstore_plperlu.control
 create mode 100644 contrib/hstore_plperl/sql/create_transform.sql
 create mode 100644 contrib/hstore_plperl/sql/hstore_plperl.sql
 create mode 100644 contrib/hstore_plpython/.gitignore
 create mode 100644 contrib/hstore_plpython/Makefile
 create mode 100644 contrib/hstore_plpython/expected/hstore_plpython.out
 create mode 100644 contrib/hstore_plpython/hstore_plpython.c
 create mode 100644 contrib/hstore_plpython/hstore_plpython2u--1.0.sql
 create mode 100644 contrib/hstore_plpython/hstore_plpython2u.control
 create mode 100644 contrib/hstore_plpython/hstore_plpython3u--1.0.sql
 create mode 100644 contrib/hstore_plpython/hstore_plpython3u.control
 create mode 100644 contrib/hstore_plpython/hstore_plpythonu--1.0.sql
 create mode 100644 contrib/hstore_plpython/hstore_plpythonu.control
 create mode 100644 contrib/hstore_plpython/sql/hstore_plpython.sql
 create mode 100644 contrib/ltree_plpython/.gitignore
 create mode 100644 contrib/ltree_plpython/Makefile
 create mode 100644 contrib/ltree_plpython/expected/ltree_plpython.out
 create mode 100644 contrib/ltree_plpython/ltree_plpython.c
 create mode 100644 contrib/ltree_plpython/ltree_plpython2u--1.0.sql
 create mode 100644 contrib/ltree_plpython/ltree_plpython2u.control
 create mode 100644 contrib/ltree_plpython/ltree_plpython3u--1.0.sql
 create mode 100644 contrib/ltree_plpython/ltree_plpython3u.control
 create mode 100644 contrib/ltree_plpython/ltree_plpythonu--1.0.sql
 create mode 100644 contrib/ltree_plpython/ltree_plpythonu.control
 create mode 100644 contrib/ltree_plpython/sql/ltree_plpython.sql
 create mode 100644 doc/src/sgml/ref/create_transform.sgml
 create mode 100644 doc/src/sgml/ref/drop_transform.sgml
 create mode 100644 src/include/catalog/pg_transform.h
 create mode 100644 src/pl/plpython/regress-python3-mangle.mk


diff --git a/contrib/Makefile b/contrib/Makefile
index 8a2a937..6495b13 100644
--- a/contrib/Makefile
+++ b/contrib/Makefile
@@ -79,6 +79,18 @@ else
 ALWAYS_SUBDIRS += sepgsql
 endif

+ifeq ($(with_perl),yes)
+SUBDIRS += hstore_plperl
+else
+ALWAYS_SUBDIRS += hstore_plperl
+endif
+
+ifeq ($(with_python),yes)
+SUBDIRS += hstore_plpython ltree_plpython
+else
+ALWAYS_SUBDIRS += hstore_plpython ltree_plpython
+endif
+
 # Missing:
 #        start-scripts    \ (does not have a makefile)

diff --git a/contrib/hstore_plperl/.gitignore b/contrib/hstore_plperl/.gitignore
new file mode 100644
index 0000000..5dcb3ff
--- /dev/null
+++ b/contrib/hstore_plperl/.gitignore
@@ -0,0 +1,4 @@
+# Generated subdirectories
+/log/
+/results/
+/tmp_check/
diff --git a/contrib/hstore_plperl/Makefile b/contrib/hstore_plperl/Makefile
new file mode 100644
index 0000000..81f037e
--- /dev/null
+++ b/contrib/hstore_plperl/Makefile
@@ -0,0 +1,23 @@
+# contrib/hstore_plperl/Makefile
+
+MODULE_big = hstore_plperl
+OBJS = hstore_plperl.o
+
+PG_CPPFLAGS = -I$(top_srcdir)/src/pl/plperl -I$(perl_archlibexp)/CORE -I$(top_srcdir)/contrib/hstore
+
+EXTENSION = hstore_plperl hstore_plperlu
+DATA = hstore_plperl--1.0.sql hstore_plperlu--1.0.sql
+
+REGRESS = hstore_plperl create_transform
+REGRESS_OPTS = --extra-install=contrib/hstore --load-extension=hstore --load-extension=plperl --load-extension=plperlu
+
+ifdef USE_PGXS
+PG_CONFIG = pg_config
+PGXS := $(shell $(PG_CONFIG) --pgxs)
+include $(PGXS)
+else
+subdir = contrib/hstore_plperl
+top_builddir = ../..
+include $(top_builddir)/src/Makefile.global
+include $(top_srcdir)/contrib/contrib-global.mk
+endif
diff --git a/contrib/hstore_plperl/expected/create_transform.out b/contrib/hstore_plperl/expected/create_transform.out
new file mode 100644
index 0000000..8111d1b
--- /dev/null
+++ b/contrib/hstore_plperl/expected/create_transform.out
@@ -0,0 +1,70 @@
+-- general regression test for transforms
+DROP EXTENSION IF EXISTS hstore CASCADE;
+NOTICE:  extension "hstore" does not exist, skipping
+DROP EXTENSION IF EXISTS plperl CASCADE;
+NOTICE:  extension "plperl" does not exist, skipping
+DROP EXTENSION IF EXISTS hstore_plperl CASCADE;
+NOTICE:  extension "hstore_plperl" does not exist, skipping
+CREATE EXTENSION hstore;
+CREATE EXTENSION plperl;
+CREATE FUNCTION hstore_to_plperl(val internal) RETURNS internal
+LANGUAGE C STRICT IMMUTABLE
+AS '$libdir/hstore_plperl';
+CREATE FUNCTION plperl_to_hstore(val internal) RETURNS hstore
+LANGUAGE C STRICT IMMUTABLE
+AS '$libdir/hstore_plperl';
+CREATE TRANSFORM FOR foo LANGUAGE plperl (FROM SQL WITH FUNCTION hstore_to_plperl(internal), TO SQL WITH FUNCTION
plperl_to_hstore(internal)); -- fail 
+ERROR:  type "foo" does not exist
+CREATE TRANSFORM FOR hstore LANGUAGE foo (FROM SQL WITH FUNCTION hstore_to_plperl(internal), TO SQL WITH FUNCTION
plperl_to_hstore(internal)); -- fail 
+ERROR:  language "foo" does not exist
+CREATE TRANSFORM FOR hstore LANGUAGE plperl (FROM SQL WITH FUNCTION hstore_out(hstore), TO SQL WITH FUNCTION
plperl_to_hstore(internal)); -- fail 
+ERROR:  return data type of FROM SQL function must be "internal"
+CREATE TRANSFORM FOR hstore LANGUAGE plperl (FROM SQL WITH FUNCTION internal_in(cstring), TO SQL WITH FUNCTION
plperl_to_hstore(internal)); -- fail 
+ERROR:  first argument of transform function must be type "internal"
+CREATE TRANSFORM FOR hstore LANGUAGE plperl (FROM SQL WITH FUNCTION hstore_to_plperl(internal), TO SQL WITH FUNCTION
plperl_to_hstore(internal)); -- ok 
+CREATE TRANSFORM FOR hstore LANGUAGE plperl (FROM SQL WITH FUNCTION hstore_to_plperl(internal), TO SQL WITH FUNCTION
plperl_to_hstore(internal)); -- fail 
+ERROR:  transform for type hstore language plperl already exists
+CREATE OR REPLACE TRANSFORM FOR hstore LANGUAGE plperl (FROM SQL WITH FUNCTION hstore_to_plperl(internal), TO SQL WITH
FUNCTIONplperl_to_hstore(internal));  -- ok 
+CREATE OR REPLACE TRANSFORM FOR hstore LANGUAGE plperl (FROM SQL WITH FUNCTION hstore_to_plperl(internal));  -- ok
+CREATE OR REPLACE TRANSFORM FOR hstore LANGUAGE plperl (TO SQL WITH FUNCTION plperl_to_hstore(internal));  -- ok
+DROP TRANSFORM FOR foo LANGUAGE plperl;
+ERROR:  type "foo" does not exist
+DROP TRANSFORM FOR hstore LANGUAGE foo;
+ERROR:  language "foo" does not exist
+DROP TRANSFORM FOR hstore LANGUAGE plperl;
+DROP TRANSFORM IF EXISTS FOR hstore LANGUAGE plperl;
+NOTICE:  transform for type hstore language plperl does not exist, skipping
+DROP FUNCTION hstore_to_plperl(val internal);
+DROP FUNCTION plperl_to_hstore(val internal);
+CREATE EXTENSION hstore_plperl;
+\dx+ hstore_plperl
+ Objects in extension "hstore_plperl"
+          Object Description
+--------------------------------------
+ function hstore_to_plperl(internal)
+ function plperl_to_hstore(internal)
+ transform for hstore language plperl
+(3 rows)
+
+ALTER EXTENSION hstore_plperl DROP TRANSFORM FOR hstore LANGUAGE plperl;
+\dx+ hstore_plperl
+Objects in extension "hstore_plperl"
+         Object Description
+-------------------------------------
+ function hstore_to_plperl(internal)
+ function plperl_to_hstore(internal)
+(2 rows)
+
+ALTER EXTENSION hstore_plperl ADD TRANSFORM FOR hstore LANGUAGE plperl;
+\dx+ hstore_plperl
+ Objects in extension "hstore_plperl"
+          Object Description
+--------------------------------------
+ function hstore_to_plperl(internal)
+ function plperl_to_hstore(internal)
+ transform for hstore language plperl
+(3 rows)
+
+DROP EXTENSION hstore CASCADE;
+NOTICE:  drop cascades to extension hstore_plperl
+DROP EXTENSION plperl CASCADE;
diff --git a/contrib/hstore_plperl/expected/hstore_plperl.out b/contrib/hstore_plperl/expected/hstore_plperl.out
new file mode 100644
index 0000000..ce31ab8
--- /dev/null
+++ b/contrib/hstore_plperl/expected/hstore_plperl.out
@@ -0,0 +1,170 @@
+CREATE EXTENSION hstore_plperl;
+CREATE EXTENSION hstore_plperlu;
+-- test hstore -> perl
+CREATE FUNCTION test1(val hstore) RETURNS int
+LANGUAGE plperlu
+AS $$
+use Data::Dumper;
+elog(INFO, Dumper($_[0]));
+return scalar(keys %{$_[0]});
+$$;
+SELECT test1('aa=>bb, cc=>NULL'::hstore);
+INFO:  $VAR1 = {
+          'cc' => undef,
+          'aa' => 'bb'
+        };
+
+CONTEXT:  PL/Perl function "test1"
+ test1
+-------
+     2
+(1 row)
+
+-- test hstore[] -> perl
+CREATE FUNCTION test1arr(val hstore[]) RETURNS int
+LANGUAGE plperlu
+AS $$
+use Data::Dumper;
+elog(INFO, Dumper($_[0]->[0], $_[0]->[1]));
+return scalar(keys %{$_[0]});
+$$;
+SELECT test1arr(array['aa=>bb, cc=>NULL'::hstore, 'dd=>ee']);
+INFO:  $VAR1 = {
+          'cc' => undef,
+          'aa' => 'bb'
+        };
+$VAR2 = {
+          'dd' => 'ee'
+        };
+
+CONTEXT:  PL/Perl function "test1arr"
+ test1arr
+----------
+        2
+(1 row)
+
+-- test perl -> hstore
+CREATE FUNCTION test2() RETURNS hstore
+LANGUAGE plperl
+AS $$
+$val = {a => 1, b => 'boo', c => undef};
+return $val;
+$$;
+SELECT test2();
+              test2
+---------------------------------
+ "a"=>"1", "b"=>"boo", "c"=>NULL
+(1 row)
+
+-- test perl -> hstore[]
+CREATE FUNCTION test2arr() RETURNS hstore[]
+LANGUAGE plperl
+AS $$
+$val = [{a => 1, b => 'boo', c => undef}, {d => 2}];
+return $val;
+$$;
+SELECT test2arr();
+                           test2arr
+--------------------------------------------------------------
+ {"\"a\"=>\"1\", \"b\"=>\"boo\", \"c\"=>NULL","\"d\"=>\"2\""}
+(1 row)
+
+-- test as part of prepare/execute
+CREATE FUNCTION test3() RETURNS void
+LANGUAGE plperlu
+AS $$
+use Data::Dumper;
+
+$rv = spi_exec_query(q{SELECT 'aa=>bb, cc=>NULL'::hstore AS col1});
+elog(INFO, Dumper($rv->{rows}[0]->{col1}));
+
+$val = {a => 1, b => 'boo', c => undef};
+$plan = spi_prepare(q{SELECT $1::text AS col1}, "hstore");
+$rv = spi_exec_prepared($plan, {}, $val);
+elog(INFO, Dumper($rv->{rows}[0]->{col1}));
+$$;
+SELECT test3();
+INFO:  $VAR1 = {
+          'cc' => undef,
+          'aa' => 'bb'
+        };
+
+CONTEXT:  PL/Perl function "test3"
+INFO:  $VAR1 = '"a"=>"1", "b"=>"boo", "c"=>NULL';
+
+CONTEXT:  PL/Perl function "test3"
+ test3
+-------
+
+(1 row)
+
+-- test inline
+DO LANGUAGE plperlu $$
+use Data::Dumper;
+
+$rv = spi_exec_query(q{SELECT 'aa=>bb, cc=>NULL'::hstore AS col1});
+elog(INFO, Dumper($rv->{rows}[0]->{col1}));
+
+$val = {a => 1, b => 'boo', c => undef};
+$plan = spi_prepare(q{SELECT $1::text AS col1}, "hstore");
+$rv = spi_exec_prepared($plan, {}, $val);
+elog(INFO, Dumper($rv->{rows}[0]->{col1}));
+$$;
+INFO:  $VAR1 = {
+          'cc' => undef,
+          'aa' => 'bb'
+        };
+
+CONTEXT:  PL/Perl anonymous code block
+INFO:  $VAR1 = '"a"=>"1", "b"=>"boo", "c"=>NULL';
+
+CONTEXT:  PL/Perl anonymous code block
+-- test trigger
+CREATE TABLE test1 (a int, b hstore);
+INSERT INTO test1 VALUES (1, 'aa=>bb, cc=>NULL');
+SELECT * FROM test1;
+ a |           b
+---+------------------------
+ 1 | "aa"=>"bb", "cc"=>NULL
+(1 row)
+
+CREATE FUNCTION test4() RETURNS trigger
+LANGUAGE plperlu
+AS $$
+use Data::Dumper;
+elog(INFO, Dumper($_TD->{new}));
+if ($_TD->{new}{a} == 1) {
+    $_TD->{new}{b} = {a => 1, b => 'boo', c => undef};
+}
+
+return "MODIFY";
+$$;
+CREATE TRIGGER test4 BEFORE UPDATE ON test1 FOR EACH ROW EXECUTE PROCEDURE test4();
+UPDATE test1 SET a = a;
+INFO:  $VAR1 = {
+          'a' => '1',
+          'b' => {
+                 'cc' => undef,
+                 'aa' => 'bb'
+               }
+        };
+
+CONTEXT:  PL/Perl function "test4"
+SELECT * FROM test1;
+ a |                b
+---+---------------------------------
+ 1 | "a"=>"1", "b"=>"boo", "c"=>NULL
+(1 row)
+
+DROP TABLE test1;
+DROP FUNCTION test1(hstore);
+DROP FUNCTION test1arr(hstore[]);
+DROP FUNCTION test2();
+DROP FUNCTION test2arr();
+DROP FUNCTION test3();
+DROP FUNCTION test4();
+DROP EXTENSION hstore_plperl;
+DROP EXTENSION hstore_plperlu;
+DROP EXTENSION hstore;
+DROP EXTENSION plperl;
+DROP EXTENSION plperlu;
diff --git a/contrib/hstore_plperl/hstore_plperl--1.0.sql b/contrib/hstore_plperl/hstore_plperl--1.0.sql
new file mode 100644
index 0000000..ea0ad76
--- /dev/null
+++ b/contrib/hstore_plperl/hstore_plperl--1.0.sql
@@ -0,0 +1,17 @@
+-- make sure the prerequisite libraries are loaded
+DO '' LANGUAGE plperl;
+SELECT NULL::hstore;
+
+
+CREATE FUNCTION hstore_to_plperl(val internal) RETURNS internal
+LANGUAGE C STRICT IMMUTABLE
+AS 'MODULE_PATHNAME';
+
+CREATE FUNCTION plperl_to_hstore(val internal) RETURNS hstore
+LANGUAGE C STRICT IMMUTABLE
+AS 'MODULE_PATHNAME';
+
+CREATE TRANSFORM FOR hstore LANGUAGE plperl (
+    FROM SQL WITH FUNCTION hstore_to_plperl(internal),
+    TO SQL WITH FUNCTION plperl_to_hstore(internal)
+);
diff --git a/contrib/hstore_plperl/hstore_plperl.c b/contrib/hstore_plperl/hstore_plperl.c
new file mode 100644
index 0000000..cdc224c
--- /dev/null
+++ b/contrib/hstore_plperl/hstore_plperl.c
@@ -0,0 +1,90 @@
+#include "postgres.h"
+#undef _
+#include "fmgr.h"
+#include "plperl.h"
+#include "plperl_helpers.h"
+#include "hstore.h"
+
+PG_MODULE_MAGIC;
+
+
+PG_FUNCTION_INFO_V1(hstore_to_plperl);
+Datum hstore_to_plperl(PG_FUNCTION_ARGS);
+
+Datum
+hstore_to_plperl(PG_FUNCTION_ARGS)
+{
+    HStore       *in = PG_GETARG_HS(0);
+    int            i;
+    int            count = HS_COUNT(in);
+    char       *base = STRPTR(in);
+    HEntry       *entries = ARRPTR(in);
+    HV           *hv;
+
+    hv = newHV();
+
+    for (i = 0; i < count; i++)
+    {
+        const char *key;
+        SV       *value;
+
+        key = pnstrdup(HS_KEY(entries, base, i), HS_KEYLEN(entries, i));
+        value = HS_VALISNULL(entries, i) ? newSV(0) : cstr2sv(pnstrdup(HS_VAL(entries, base,i), HS_VALLEN(entries,
i)));
+
+        (void) hv_store(hv, key, strlen(key), value, 0);
+    }
+
+    return PointerGetDatum(newRV((SV *) hv));
+}
+
+
+PG_FUNCTION_INFO_V1(plperl_to_hstore);
+Datum plperl_to_hstore(PG_FUNCTION_ARGS);
+
+Datum
+plperl_to_hstore(PG_FUNCTION_ARGS)
+{
+    HV           *hv;
+    HE           *he;
+    int32        buflen;
+    int32        i;
+    int32        pcount;
+    HStore       *out;
+    Pairs       *pairs;
+
+    hv = (HV *) SvRV((SV *) PG_GETARG_POINTER(0));
+
+    pcount = hv_iterinit(hv);
+
+    pairs = palloc(pcount * sizeof(Pairs));
+
+    i = 0;
+    while ((he = hv_iternext(hv)))
+    {
+        char     *key = sv2cstr(HeSVKEY_force(he));
+        SV         *value = HeVAL(he);
+
+        pairs[i].key = pstrdup(key);
+        pairs[i].keylen = hstoreCheckKeyLen(strlen(pairs[i].key));
+        pairs[i].needfree = true;
+
+        if (!SvOK(value))
+        {
+            pairs[i].val = NULL;
+            pairs[i].vallen = 0;
+            pairs[i].isnull = true;
+        }
+        else
+        {
+            pairs[i].val = pstrdup(sv2cstr(value));
+            pairs[i].vallen = hstoreCheckValLen(strlen(pairs[i].val));
+            pairs[i].isnull = false;
+        }
+
+        i++;
+    }
+
+    pcount = hstoreUniquePairs(pairs, pcount, &buflen);
+    out = hstorePairs(pairs, pcount, buflen);
+    PG_RETURN_POINTER(out);
+}
diff --git a/contrib/hstore_plperl/hstore_plperl.control b/contrib/hstore_plperl/hstore_plperl.control
new file mode 100644
index 0000000..16277f6
--- /dev/null
+++ b/contrib/hstore_plperl/hstore_plperl.control
@@ -0,0 +1,6 @@
+# hstore_plperl extension
+comment = 'transform between hstore and plperl'
+default_version = '1.0'
+module_pathname = '$libdir/hstore_plperl'
+relocatable = true
+requires = 'hstore,plperl'
diff --git a/contrib/hstore_plperl/hstore_plperlu--1.0.sql b/contrib/hstore_plperl/hstore_plperlu--1.0.sql
new file mode 100644
index 0000000..46ad35c
--- /dev/null
+++ b/contrib/hstore_plperl/hstore_plperlu--1.0.sql
@@ -0,0 +1,17 @@
+-- make sure the prerequisite libraries are loaded
+DO '' LANGUAGE plperlu;
+SELECT NULL::hstore;
+
+
+CREATE FUNCTION hstore_to_plperlu(val internal) RETURNS internal
+LANGUAGE C STRICT IMMUTABLE
+AS 'MODULE_PATHNAME', 'hstore_to_plperl';
+
+CREATE FUNCTION plperlu_to_hstore(val internal) RETURNS hstore
+LANGUAGE C STRICT IMMUTABLE
+AS 'MODULE_PATHNAME', 'plperl_to_hstore';
+
+CREATE TRANSFORM FOR hstore LANGUAGE plperlu (
+    FROM SQL WITH FUNCTION hstore_to_plperlu(internal),
+    TO SQL WITH FUNCTION plperlu_to_hstore(internal)
+);
diff --git a/contrib/hstore_plperl/hstore_plperlu.control b/contrib/hstore_plperl/hstore_plperlu.control
new file mode 100644
index 0000000..c8d43b4
--- /dev/null
+++ b/contrib/hstore_plperl/hstore_plperlu.control
@@ -0,0 +1,6 @@
+# hstore_plperlu extension
+comment = 'transform between hstore and plperlu'
+default_version = '1.0'
+module_pathname = '$libdir/hstore_plperl'
+relocatable = true
+requires = 'hstore,plperlu'
diff --git a/contrib/hstore_plperl/sql/create_transform.sql b/contrib/hstore_plperl/sql/create_transform.sql
new file mode 100644
index 0000000..4f615e7
--- /dev/null
+++ b/contrib/hstore_plperl/sql/create_transform.sql
@@ -0,0 +1,45 @@
+-- general regression test for transforms
+
+DROP EXTENSION IF EXISTS hstore CASCADE;
+DROP EXTENSION IF EXISTS plperl CASCADE;
+DROP EXTENSION IF EXISTS hstore_plperl CASCADE;
+
+CREATE EXTENSION hstore;
+CREATE EXTENSION plperl;
+
+CREATE FUNCTION hstore_to_plperl(val internal) RETURNS internal
+LANGUAGE C STRICT IMMUTABLE
+AS '$libdir/hstore_plperl';
+
+CREATE FUNCTION plperl_to_hstore(val internal) RETURNS hstore
+LANGUAGE C STRICT IMMUTABLE
+AS '$libdir/hstore_plperl';
+
+CREATE TRANSFORM FOR foo LANGUAGE plperl (FROM SQL WITH FUNCTION hstore_to_plperl(internal), TO SQL WITH FUNCTION
plperl_to_hstore(internal)); -- fail 
+CREATE TRANSFORM FOR hstore LANGUAGE foo (FROM SQL WITH FUNCTION hstore_to_plperl(internal), TO SQL WITH FUNCTION
plperl_to_hstore(internal)); -- fail 
+CREATE TRANSFORM FOR hstore LANGUAGE plperl (FROM SQL WITH FUNCTION hstore_out(hstore), TO SQL WITH FUNCTION
plperl_to_hstore(internal)); -- fail 
+CREATE TRANSFORM FOR hstore LANGUAGE plperl (FROM SQL WITH FUNCTION internal_in(cstring), TO SQL WITH FUNCTION
plperl_to_hstore(internal)); -- fail 
+
+CREATE TRANSFORM FOR hstore LANGUAGE plperl (FROM SQL WITH FUNCTION hstore_to_plperl(internal), TO SQL WITH FUNCTION
plperl_to_hstore(internal)); -- ok 
+CREATE TRANSFORM FOR hstore LANGUAGE plperl (FROM SQL WITH FUNCTION hstore_to_plperl(internal), TO SQL WITH FUNCTION
plperl_to_hstore(internal)); -- fail 
+CREATE OR REPLACE TRANSFORM FOR hstore LANGUAGE plperl (FROM SQL WITH FUNCTION hstore_to_plperl(internal), TO SQL WITH
FUNCTIONplperl_to_hstore(internal));  -- ok 
+CREATE OR REPLACE TRANSFORM FOR hstore LANGUAGE plperl (FROM SQL WITH FUNCTION hstore_to_plperl(internal));  -- ok
+CREATE OR REPLACE TRANSFORM FOR hstore LANGUAGE plperl (TO SQL WITH FUNCTION plperl_to_hstore(internal));  -- ok
+
+DROP TRANSFORM FOR foo LANGUAGE plperl;
+DROP TRANSFORM FOR hstore LANGUAGE foo;
+DROP TRANSFORM FOR hstore LANGUAGE plperl;
+DROP TRANSFORM IF EXISTS FOR hstore LANGUAGE plperl;
+
+DROP FUNCTION hstore_to_plperl(val internal);
+DROP FUNCTION plperl_to_hstore(val internal);
+
+CREATE EXTENSION hstore_plperl;
+\dx+ hstore_plperl
+ALTER EXTENSION hstore_plperl DROP TRANSFORM FOR hstore LANGUAGE plperl;
+\dx+ hstore_plperl
+ALTER EXTENSION hstore_plperl ADD TRANSFORM FOR hstore LANGUAGE plperl;
+\dx+ hstore_plperl
+
+DROP EXTENSION hstore CASCADE;
+DROP EXTENSION plperl CASCADE;
diff --git a/contrib/hstore_plperl/sql/hstore_plperl.sql b/contrib/hstore_plperl/sql/hstore_plperl.sql
new file mode 100644
index 0000000..4796050
--- /dev/null
+++ b/contrib/hstore_plperl/sql/hstore_plperl.sql
@@ -0,0 +1,120 @@
+CREATE EXTENSION hstore_plperl;
+CREATE EXTENSION hstore_plperlu;
+
+
+-- test hstore -> perl
+CREATE FUNCTION test1(val hstore) RETURNS int
+LANGUAGE plperlu
+AS $$
+use Data::Dumper;
+elog(INFO, Dumper($_[0]));
+return scalar(keys %{$_[0]});
+$$;
+
+SELECT test1('aa=>bb, cc=>NULL'::hstore);
+
+
+-- test hstore[] -> perl
+CREATE FUNCTION test1arr(val hstore[]) RETURNS int
+LANGUAGE plperlu
+AS $$
+use Data::Dumper;
+elog(INFO, Dumper($_[0]->[0], $_[0]->[1]));
+return scalar(keys %{$_[0]});
+$$;
+
+SELECT test1arr(array['aa=>bb, cc=>NULL'::hstore, 'dd=>ee']);
+
+
+-- test perl -> hstore
+CREATE FUNCTION test2() RETURNS hstore
+LANGUAGE plperl
+AS $$
+$val = {a => 1, b => 'boo', c => undef};
+return $val;
+$$;
+
+SELECT test2();
+
+
+-- test perl -> hstore[]
+CREATE FUNCTION test2arr() RETURNS hstore[]
+LANGUAGE plperl
+AS $$
+$val = [{a => 1, b => 'boo', c => undef}, {d => 2}];
+return $val;
+$$;
+
+SELECT test2arr();
+
+
+-- test as part of prepare/execute
+CREATE FUNCTION test3() RETURNS void
+LANGUAGE plperlu
+AS $$
+use Data::Dumper;
+
+$rv = spi_exec_query(q{SELECT 'aa=>bb, cc=>NULL'::hstore AS col1});
+elog(INFO, Dumper($rv->{rows}[0]->{col1}));
+
+$val = {a => 1, b => 'boo', c => undef};
+$plan = spi_prepare(q{SELECT $1::text AS col1}, "hstore");
+$rv = spi_exec_prepared($plan, {}, $val);
+elog(INFO, Dumper($rv->{rows}[0]->{col1}));
+$$;
+
+SELECT test3();
+
+
+-- test inline
+DO LANGUAGE plperlu $$
+use Data::Dumper;
+
+$rv = spi_exec_query(q{SELECT 'aa=>bb, cc=>NULL'::hstore AS col1});
+elog(INFO, Dumper($rv->{rows}[0]->{col1}));
+
+$val = {a => 1, b => 'boo', c => undef};
+$plan = spi_prepare(q{SELECT $1::text AS col1}, "hstore");
+$rv = spi_exec_prepared($plan, {}, $val);
+elog(INFO, Dumper($rv->{rows}[0]->{col1}));
+$$;
+
+
+-- test trigger
+CREATE TABLE test1 (a int, b hstore);
+INSERT INTO test1 VALUES (1, 'aa=>bb, cc=>NULL');
+SELECT * FROM test1;
+
+CREATE FUNCTION test4() RETURNS trigger
+LANGUAGE plperlu
+AS $$
+use Data::Dumper;
+elog(INFO, Dumper($_TD->{new}));
+if ($_TD->{new}{a} == 1) {
+    $_TD->{new}{b} = {a => 1, b => 'boo', c => undef};
+}
+
+return "MODIFY";
+$$;
+
+CREATE TRIGGER test4 BEFORE UPDATE ON test1 FOR EACH ROW EXECUTE PROCEDURE test4();
+
+UPDATE test1 SET a = a;
+SELECT * FROM test1;
+
+
+DROP TABLE test1;
+
+DROP FUNCTION test1(hstore);
+DROP FUNCTION test1arr(hstore[]);
+DROP FUNCTION test2();
+DROP FUNCTION test2arr();
+DROP FUNCTION test3();
+DROP FUNCTION test4();
+
+
+DROP EXTENSION hstore_plperl;
+DROP EXTENSION hstore_plperlu;
+DROP EXTENSION hstore;
+DROP EXTENSION plperl;
+DROP EXTENSION plperlu;
diff --git a/contrib/hstore_plpython/.gitignore b/contrib/hstore_plpython/.gitignore
new file mode 100644
index 0000000..5dcb3ff
--- /dev/null
+++ b/contrib/hstore_plpython/.gitignore
@@ -0,0 +1,4 @@
+# Generated subdirectories
+/log/
+/results/
+/tmp_check/
diff --git a/contrib/hstore_plpython/Makefile b/contrib/hstore_plpython/Makefile
new file mode 100644
index 0000000..65ddcf7
--- /dev/null
+++ b/contrib/hstore_plpython/Makefile
@@ -0,0 +1,30 @@
+# contrib/hstore_plpython/Makefile
+
+MODULE_big = hstore_plpython$(python_majorversion)
+OBJS = hstore_plpython.o
+
+PG_CPPFLAGS = -I$(top_srcdir)/src/pl/plpython $(python_includespec) -I$(top_srcdir)/contrib/hstore
+
+EXTENSION = hstore_plpythonu hstore_plpython2u hstore_plpython3u
+DATA = hstore_plpythonu--1.0.sql hstore_plpython2u--1.0.sql hstore_plpython3u--1.0.sql
+
+REGRESS = hstore_plpython
+REGRESS_PLPYTHON3_MANGLE := $(REGRESS)
+
+ifdef USE_PGXS
+PG_CONFIG = pg_config
+PGXS := $(shell $(PG_CONFIG) --pgxs)
+include $(PGXS)
+else
+subdir = contrib/hstore_plpython
+top_builddir = ../..
+include $(top_builddir)/src/Makefile.global
+include $(top_srcdir)/contrib/contrib-global.mk
+endif
+
+REGRESS_OPTS = --extra-install=contrib/hstore --load-extension=hstore
+ifeq ($(python_majorversion),2)
+REGRESS_OPTS += --load-extension=plpythonu --load-extension=hstore_plpythonu
+endif
+
+include $(top_srcdir)/src/pl/plpython/regress-python3-mangle.mk
diff --git a/contrib/hstore_plpython/expected/hstore_plpython.out
b/contrib/hstore_plpython/expected/hstore_plpython.out
new file mode 100644
index 0000000..1bc59dd
--- /dev/null
+++ b/contrib/hstore_plpython/expected/hstore_plpython.out
@@ -0,0 +1,137 @@
+CREATE EXTENSION plpython2u;
+CREATE EXTENSION hstore_plpython2u;
+-- test hstore -> python
+CREATE FUNCTION test1(val hstore) RETURNS int
+LANGUAGE plpythonu
+AS $$
+plpy.info(repr(val))
+return len(val)
+$$;
+SELECT test1('aa=>bb, cc=>NULL'::hstore);
+INFO:  {'aa': 'bb', 'cc': None}
+CONTEXT:  PL/Python function "test1"
+ test1
+-------
+     2
+(1 row)
+
+-- the same with the versioned language name
+CREATE FUNCTION test1n(val hstore) RETURNS int
+LANGUAGE plpython2u
+AS $$
+plpy.info(repr(val))
+return len(val)
+$$;
+SELECT test1n('aa=>bb, cc=>NULL'::hstore);
+INFO:  {'aa': 'bb', 'cc': None}
+CONTEXT:  PL/Python function "test1n"
+ test1n
+--------
+      2
+(1 row)
+
+-- test hstore[] -> python
+ CREATE FUNCTION test1arr(val hstore[]) RETURNS int
+ LANGUAGE plpythonu
+ AS $$
+ plpy.info(repr(val))
+ return len(val)
+ $$;
+ SELECT test1arr(array['aa=>bb, cc=>NULL'::hstore, 'dd=>ee']);
+INFO:  [{'aa': 'bb', 'cc': None}, {'dd': 'ee'}]
+CONTEXT:  PL/Python function "test1arr"
+ test1arr
+----------
+        2
+(1 row)
+
+-- test python -> hstore
+CREATE FUNCTION test2() RETURNS hstore
+LANGUAGE plpythonu
+AS $$
+val = {'a': 1, 'b': 'boo', 'c': None}
+return val
+$$;
+SELECT test2();
+              test2
+---------------------------------
+ "a"=>"1", "b"=>"boo", "c"=>NULL
+(1 row)
+
+-- test python -> hstore[]
+ CREATE FUNCTION test2arr() RETURNS hstore[]
+ LANGUAGE plpythonu
+ AS $$
+ val = [{'a': 1, 'b': 'boo', 'c': None}, {'d': 2}]
+ return val
+ $$;
+ SELECT test2arr();
+                           test2arr
+--------------------------------------------------------------
+ {"\"a\"=>\"1\", \"b\"=>\"boo\", \"c\"=>NULL","\"d\"=>\"2\""}
+(1 row)
+
+-- test as part of prepare/execute
+CREATE FUNCTION test3() RETURNS void
+LANGUAGE plpythonu
+AS $$
+rv = plpy.execute("SELECT 'aa=>bb, cc=>NULL'::hstore AS col1")
+plpy.info(repr(rv[0]["col1"]))
+
+val = {'a': 1, 'b': 'boo', 'c': None}
+plan = plpy.prepare("SELECT $1::text AS col1", ["hstore"])
+rv = plpy.execute(plan, [val])
+plpy.info(repr(rv[0]["col1"]))
+$$;
+SELECT test3();
+INFO:  {'aa': 'bb', 'cc': None}
+CONTEXT:  PL/Python function "test3"
+INFO:  '"a"=>"1", "b"=>"boo", "c"=>NULL'
+CONTEXT:  PL/Python function "test3"
+ test3
+-------
+
+(1 row)
+
+-- test inline
+DO LANGUAGE plpythonu $$
+rv = plpy.execute("SELECT 'aa=>bb, cc=>NULL'::hstore AS col1")
+plpy.info(repr(rv[0]["col1"]))
+
+val = {'a': 1, 'b': 'boo', 'c': None}
+plan = plpy.prepare("SELECT $1::text AS col1", ["hstore"])
+rv = plpy.execute(plan, [val])
+plpy.info(repr(rv[0]["col1"]))
+$$;
+INFO:  {'aa': 'bb', 'cc': None}
+CONTEXT:  PL/Python anonymous code block
+INFO:  '"a"=>"1", "b"=>"boo", "c"=>NULL'
+CONTEXT:  PL/Python anonymous code block
+-- test trigger
+CREATE TABLE test1 (a int, b hstore);
+INSERT INTO test1 VALUES (1, 'aa=>bb, cc=>NULL');
+SELECT * FROM test1;
+ a |           b
+---+------------------------
+ 1 | "aa"=>"bb", "cc"=>NULL
+(1 row)
+
+CREATE FUNCTION test4() RETURNS trigger
+LANGUAGE plpythonu
+AS $$
+plpy.info("Trigger row: %r" % TD["new"])
+if TD["new"]["a"] == 1:
+    TD["new"]["b"] = {'a': 1, 'b': 'boo', 'c': None}
+
+return "MODIFY"
+$$;
+CREATE TRIGGER test4 BEFORE UPDATE ON test1 FOR EACH ROW EXECUTE PROCEDURE test4();
+UPDATE test1 SET a = a;
+INFO:  Trigger row: {'a': 1, 'b': {'aa': 'bb', 'cc': None}}
+CONTEXT:  PL/Python function "test4"
+SELECT * FROM test1;
+ a |                b
+---+---------------------------------
+ 1 | "a"=>"1", "b"=>"boo", "c"=>NULL
+(1 row)
+
diff --git a/contrib/hstore_plpython/hstore_plpython.c b/contrib/hstore_plpython/hstore_plpython.c
new file mode 100644
index 0000000..92cd4f8
--- /dev/null
+++ b/contrib/hstore_plpython/hstore_plpython.c
@@ -0,0 +1,116 @@
+#include "postgres.h"
+#include "fmgr.h"
+#include "plpython.h"
+#include "plpy_typeio.h"
+#include "hstore.h"
+
+PG_MODULE_MAGIC;
+
+
+PG_FUNCTION_INFO_V1(hstore_to_plpython);
+Datum hstore_to_plpython(PG_FUNCTION_ARGS);
+
+Datum
+hstore_to_plpython(PG_FUNCTION_ARGS)
+{
+    HStore       *in = PG_GETARG_HS(0);
+    int            i;
+    int            count = HS_COUNT(in);
+    char       *base = STRPTR(in);
+    HEntry       *entries = ARRPTR(in);
+    PyObject   *dict;
+
+    dict = PyDict_New();
+
+    for (i = 0; i < count; i++)
+    {
+        PyObject   *key;
+
+        key = PyString_FromStringAndSize(HS_KEY(entries, base, i), HS_KEYLEN(entries, i));
+        if (HS_VALISNULL(entries, i))
+            PyDict_SetItem(dict, key, Py_None);
+        else
+        {
+            PyObject *value;
+
+            value = PyString_FromStringAndSize(HS_VAL(entries, base,i), HS_VALLEN(entries, i));
+            PyDict_SetItem(dict, key, value);
+            Py_XDECREF(value);
+        }
+        Py_XDECREF(key);
+    }
+
+    return PointerGetDatum(dict);
+}
+
+
+PG_FUNCTION_INFO_V1(plpython_to_hstore);
+Datum plpython_to_hstore(PG_FUNCTION_ARGS);
+
+Datum
+plpython_to_hstore(PG_FUNCTION_ARGS)
+{
+    PyObject   *dict;
+    volatile PyObject *items_v = NULL;
+    int32        pcount;
+    HStore       *out;
+
+    dict = (PyObject *) PG_GETARG_POINTER(0);
+    if (!PyMapping_Check(dict))
+        ereport(ERROR,
+                (errcode(ERRCODE_WRONG_OBJECT_TYPE),
+                 errmsg("not a Python mapping")));
+
+    pcount = PyMapping_Size(dict);
+    items_v = PyMapping_Items(dict);
+
+    PG_TRY();
+    {
+        int32        buflen;
+        int32        i;
+        Pairs       *pairs;
+        PyObject   *items = (PyObject *) items_v;
+
+        pairs = palloc(pcount * sizeof(*pairs));
+
+        for (i = 0; i < pcount; i++)
+        {
+            PyObject *tuple;
+            PyObject *key;
+            PyObject *value;
+
+            tuple = PyList_GetItem(items, i);
+            key = PyTuple_GetItem(tuple, 0);
+            value = PyTuple_GetItem(tuple, 1);
+
+            pairs[i].key = PLyObject_AsString(key);
+            pairs[i].keylen = hstoreCheckKeyLen(strlen(pairs[i].key));
+            pairs[i].needfree = true;
+
+            if (value == Py_None)
+            {
+                pairs[i].val = NULL;
+                pairs[i].vallen = 0;
+                pairs[i].isnull = true;
+            }
+            else
+            {
+                pairs[i].val = PLyObject_AsString(value);
+                pairs[i].vallen = hstoreCheckValLen(strlen(pairs[i].val));
+                pairs[i].isnull = false;
+            }
+        }
+        Py_DECREF(items_v);
+
+        pcount = hstoreUniquePairs(pairs, pcount, &buflen);
+        out = hstorePairs(pairs, pcount, buflen);
+    }
+    PG_CATCH();
+    {
+        Py_DECREF(items_v);
+        PG_RE_THROW();
+    }
+    PG_END_TRY();
+
+    PG_RETURN_POINTER(out);
+}
diff --git a/contrib/hstore_plpython/hstore_plpython2u--1.0.sql b/contrib/hstore_plpython/hstore_plpython2u--1.0.sql
new file mode 100644
index 0000000..c998de5
--- /dev/null
+++ b/contrib/hstore_plpython/hstore_plpython2u--1.0.sql
@@ -0,0 +1,19 @@
+-- make sure the prerequisite libraries are loaded
+DO '1' LANGUAGE plpython2u;
+SELECT NULL::hstore;
+
+
+CREATE FUNCTION hstore_to_plpython2(val internal) RETURNS internal
+LANGUAGE C STRICT IMMUTABLE
+AS 'MODULE_PATHNAME', 'hstore_to_plpython';
+
+CREATE FUNCTION plpython2_to_hstore(val internal) RETURNS hstore
+LANGUAGE C STRICT IMMUTABLE
+AS 'MODULE_PATHNAME', 'plpython_to_hstore';
+
+CREATE TRANSFORM FOR hstore LANGUAGE plpython2u (
+    FROM SQL WITH FUNCTION hstore_to_plpython2(internal),
+    TO SQL WITH FUNCTION plpython2_to_hstore(internal)
+);
+
+COMMENT ON TRANSFORM FOR hstore LANGUAGE plpython2u IS 'transform between hstore and Python dict';
diff --git a/contrib/hstore_plpython/hstore_plpython2u.control b/contrib/hstore_plpython/hstore_plpython2u.control
new file mode 100644
index 0000000..ed90567
--- /dev/null
+++ b/contrib/hstore_plpython/hstore_plpython2u.control
@@ -0,0 +1,6 @@
+# hstore_plpython2u extension
+comment = 'transform between hstore and plpython2u'
+default_version = '1.0'
+module_pathname = '$libdir/hstore_plpython2'
+relocatable = true
+requires = 'hstore,plpython2u'
diff --git a/contrib/hstore_plpython/hstore_plpython3u--1.0.sql b/contrib/hstore_plpython/hstore_plpython3u--1.0.sql
new file mode 100644
index 0000000..61d0e47
--- /dev/null
+++ b/contrib/hstore_plpython/hstore_plpython3u--1.0.sql
@@ -0,0 +1,19 @@
+-- make sure the prerequisite libraries are loaded
+DO '1' LANGUAGE plpython3u;
+SELECT NULL::hstore;
+
+
+CREATE FUNCTION hstore_to_plpython3(val internal) RETURNS internal
+LANGUAGE C STRICT IMMUTABLE
+AS 'MODULE_PATHNAME', 'hstore_to_plpython';
+
+CREATE FUNCTION plpython3_to_hstore(val internal) RETURNS hstore
+LANGUAGE C STRICT IMMUTABLE
+AS 'MODULE_PATHNAME', 'plpython_to_hstore';
+
+CREATE TRANSFORM FOR hstore LANGUAGE plpython3u (
+    FROM SQL WITH FUNCTION hstore_to_plpython3(internal),
+    TO SQL WITH FUNCTION plpython3_to_hstore(internal)
+);
+
+COMMENT ON TRANSFORM FOR hstore LANGUAGE plpython3u IS 'transform between hstore and Python dict';
diff --git a/contrib/hstore_plpython/hstore_plpython3u.control b/contrib/hstore_plpython/hstore_plpython3u.control
new file mode 100644
index 0000000..d86f38e
--- /dev/null
+++ b/contrib/hstore_plpython/hstore_plpython3u.control
@@ -0,0 +1,6 @@
+# hstore_plpython3u extension
+comment = 'transform between hstore and plpython3u'
+default_version = '1.0'
+module_pathname = '$libdir/hstore_plpython3'
+relocatable = true
+requires = 'hstore,plpython3u'
diff --git a/contrib/hstore_plpython/hstore_plpythonu--1.0.sql b/contrib/hstore_plpython/hstore_plpythonu--1.0.sql
new file mode 100644
index 0000000..6acb97a
--- /dev/null
+++ b/contrib/hstore_plpython/hstore_plpythonu--1.0.sql
@@ -0,0 +1,19 @@
+-- make sure the prerequisite libraries are loaded
+DO '1' LANGUAGE plpythonu;
+SELECT NULL::hstore;
+
+
+CREATE FUNCTION hstore_to_plpython(val internal) RETURNS internal
+LANGUAGE C STRICT IMMUTABLE
+AS 'MODULE_PATHNAME';
+
+CREATE FUNCTION plpython_to_hstore(val internal) RETURNS hstore
+LANGUAGE C STRICT IMMUTABLE
+AS 'MODULE_PATHNAME';
+
+CREATE TRANSFORM FOR hstore LANGUAGE plpythonu (
+    FROM SQL WITH FUNCTION hstore_to_plpython(internal),
+    TO SQL WITH FUNCTION plpython_to_hstore(internal)
+);
+
+COMMENT ON TRANSFORM FOR hstore LANGUAGE plpythonu IS 'transform between hstore and Python dict';
diff --git a/contrib/hstore_plpython/hstore_plpythonu.control b/contrib/hstore_plpython/hstore_plpythonu.control
new file mode 100644
index 0000000..8e9b35e
--- /dev/null
+++ b/contrib/hstore_plpython/hstore_plpythonu.control
@@ -0,0 +1,6 @@
+# hstore_plpythonu extension
+comment = 'transform between hstore and plpythonu'
+default_version = '1.0'
+module_pathname = '$libdir/hstore_plpython2'
+relocatable = true
+requires = 'hstore,plpythonu'
diff --git a/contrib/hstore_plpython/sql/hstore_plpython.sql b/contrib/hstore_plpython/sql/hstore_plpython.sql
new file mode 100644
index 0000000..566cdc5
--- /dev/null
+++ b/contrib/hstore_plpython/sql/hstore_plpython.sql
@@ -0,0 +1,106 @@
+CREATE EXTENSION plpython2u;
+CREATE EXTENSION hstore_plpython2u;
+
+
+-- test hstore -> python
+CREATE FUNCTION test1(val hstore) RETURNS int
+LANGUAGE plpythonu
+AS $$
+plpy.info(repr(val))
+return len(val)
+$$;
+
+SELECT test1('aa=>bb, cc=>NULL'::hstore);
+
+
+-- the same with the versioned language name
+CREATE FUNCTION test1n(val hstore) RETURNS int
+LANGUAGE plpython2u
+AS $$
+plpy.info(repr(val))
+return len(val)
+$$;
+
+SELECT test1n('aa=>bb, cc=>NULL'::hstore);
+
+
+-- test hstore[] -> python
+ CREATE FUNCTION test1arr(val hstore[]) RETURNS int
+ LANGUAGE plpythonu
+ AS $$
+ plpy.info(repr(val))
+ return len(val)
+ $$;
+
+ SELECT test1arr(array['aa=>bb, cc=>NULL'::hstore, 'dd=>ee']);
+
+
+-- test python -> hstore
+CREATE FUNCTION test2() RETURNS hstore
+LANGUAGE plpythonu
+AS $$
+val = {'a': 1, 'b': 'boo', 'c': None}
+return val
+$$;
+
+SELECT test2();
+
+
+-- test python -> hstore[]
+ CREATE FUNCTION test2arr() RETURNS hstore[]
+ LANGUAGE plpythonu
+ AS $$
+ val = [{'a': 1, 'b': 'boo', 'c': None}, {'d': 2}]
+ return val
+ $$;
+
+ SELECT test2arr();
+
+
+-- test as part of prepare/execute
+CREATE FUNCTION test3() RETURNS void
+LANGUAGE plpythonu
+AS $$
+rv = plpy.execute("SELECT 'aa=>bb, cc=>NULL'::hstore AS col1")
+plpy.info(repr(rv[0]["col1"]))
+
+val = {'a': 1, 'b': 'boo', 'c': None}
+plan = plpy.prepare("SELECT $1::text AS col1", ["hstore"])
+rv = plpy.execute(plan, [val])
+plpy.info(repr(rv[0]["col1"]))
+$$;
+
+SELECT test3();
+
+
+-- test inline
+DO LANGUAGE plpythonu $$
+rv = plpy.execute("SELECT 'aa=>bb, cc=>NULL'::hstore AS col1")
+plpy.info(repr(rv[0]["col1"]))
+
+val = {'a': 1, 'b': 'boo', 'c': None}
+plan = plpy.prepare("SELECT $1::text AS col1", ["hstore"])
+rv = plpy.execute(plan, [val])
+plpy.info(repr(rv[0]["col1"]))
+$$;
+
+
+-- test trigger
+CREATE TABLE test1 (a int, b hstore);
+INSERT INTO test1 VALUES (1, 'aa=>bb, cc=>NULL');
+SELECT * FROM test1;
+
+CREATE FUNCTION test4() RETURNS trigger
+LANGUAGE plpythonu
+AS $$
+plpy.info("Trigger row: %r" % TD["new"])
+if TD["new"]["a"] == 1:
+    TD["new"]["b"] = {'a': 1, 'b': 'boo', 'c': None}
+
+return "MODIFY"
+$$;
+
+CREATE TRIGGER test4 BEFORE UPDATE ON test1 FOR EACH ROW EXECUTE PROCEDURE test4();
+
+UPDATE test1 SET a = a;
+SELECT * FROM test1;
diff --git a/contrib/ltree_plpython/.gitignore b/contrib/ltree_plpython/.gitignore
new file mode 100644
index 0000000..5dcb3ff
--- /dev/null
+++ b/contrib/ltree_plpython/.gitignore
@@ -0,0 +1,4 @@
+# Generated subdirectories
+/log/
+/results/
+/tmp_check/
diff --git a/contrib/ltree_plpython/Makefile b/contrib/ltree_plpython/Makefile
new file mode 100644
index 0000000..34e03ce
--- /dev/null
+++ b/contrib/ltree_plpython/Makefile
@@ -0,0 +1,30 @@
+# contrib/ltree_plpython/Makefile
+
+MODULE_big = ltree_plpython$(python_majorversion)
+OBJS = ltree_plpython.o
+
+PG_CPPFLAGS = -I$(top_srcdir)/src/pl/plpython $(python_includespec) -I$(top_srcdir)/contrib/ltree
+
+EXTENSION = ltree_plpythonu ltree_plpython2u ltree_plpython3u
+DATA = ltree_plpythonu--1.0.sql ltree_plpython2u--1.0.sql ltree_plpython3u--1.0.sql
+
+REGRESS = ltree_plpython
+REGRESS_PLPYTHON3_MANGLE := $(REGRESS)
+
+ifdef USE_PGXS
+PG_CONFIG = pg_config
+PGXS := $(shell $(PG_CONFIG) --pgxs)
+include $(PGXS)
+else
+subdir = contrib/ltree_plpython
+top_builddir = ../..
+include $(top_builddir)/src/Makefile.global
+include $(top_srcdir)/contrib/contrib-global.mk
+endif
+
+REGRESS_OPTS = --extra-install=contrib/ltree --load-extension=ltree
+ifeq ($(python_majorversion),2)
+REGRESS_OPTS += --load-extension=plpythonu --load-extension=ltree_plpythonu
+endif
+
+include $(top_srcdir)/src/pl/plpython/regress-python3-mangle.mk
diff --git a/contrib/ltree_plpython/expected/ltree_plpython.out b/contrib/ltree_plpython/expected/ltree_plpython.out
new file mode 100644
index 0000000..6626fe9
--- /dev/null
+++ b/contrib/ltree_plpython/expected/ltree_plpython.out
@@ -0,0 +1,42 @@
+CREATE EXTENSION plpython2u;
+CREATE EXTENSION ltree_plpython2u;
+CREATE FUNCTION test1(val ltree) RETURNS int
+LANGUAGE plpythonu
+AS $$
+plpy.info(repr(val))
+return len(val)
+$$;
+SELECT test1('aa.bb.cc'::ltree);
+INFO:  ['aa', 'bb', 'cc']
+CONTEXT:  PL/Python function "test1"
+ test1
+-------
+     3
+(1 row)
+
+CREATE FUNCTION test1n(val ltree) RETURNS int
+LANGUAGE plpython2u
+AS $$
+plpy.info(repr(val))
+return len(val)
+$$;
+SELECT test1n('aa.bb.cc'::ltree);
+INFO:  ['aa', 'bb', 'cc']
+CONTEXT:  PL/Python function "test1n"
+ test1n
+--------
+      3
+(1 row)
+
+CREATE FUNCTION test2() RETURNS ltree
+LANGUAGE plpythonu
+AS $$
+return ['foo', 'bar', 'baz']
+$$;
+-- plpython to ltree is not yet implemented, so this will fail,
+-- because it will try to parse the Python list as an ltree input
+-- string.
+SELECT test2();
+ERROR:  syntax error at position 0
+CONTEXT:  while creating return value
+PL/Python function "test2"
diff --git a/contrib/ltree_plpython/ltree_plpython.c b/contrib/ltree_plpython/ltree_plpython.c
new file mode 100644
index 0000000..111e3e3
--- /dev/null
+++ b/contrib/ltree_plpython/ltree_plpython.c
@@ -0,0 +1,32 @@
+#include "postgres.h"
+#include "fmgr.h"
+#include "plpython.h"
+#include "ltree.h"
+
+PG_MODULE_MAGIC;
+
+
+PG_FUNCTION_INFO_V1(ltree_to_plpython);
+Datum ltree_to_plpython(PG_FUNCTION_ARGS);
+
+Datum
+ltree_to_plpython(PG_FUNCTION_ARGS)
+{
+    ltree       *in = PG_GETARG_LTREE(0);
+    int            i;
+    PyObject   *list;
+    ltree_level *curlevel;
+
+    list = PyList_New(in->numlevel);
+
+    curlevel = LTREE_FIRST(in);
+    for (i = 0; i < in->numlevel; i++)
+    {
+        PyList_SetItem(list, i, PyString_FromStringAndSize(curlevel->name, curlevel->len));
+        curlevel = LEVEL_NEXT(curlevel);
+    }
+
+    PG_FREE_IF_COPY(in, 0);
+
+    return PointerGetDatum(list);
+}
diff --git a/contrib/ltree_plpython/ltree_plpython2u--1.0.sql b/contrib/ltree_plpython/ltree_plpython2u--1.0.sql
new file mode 100644
index 0000000..29a12d4
--- /dev/null
+++ b/contrib/ltree_plpython/ltree_plpython2u--1.0.sql
@@ -0,0 +1,12 @@
+-- make sure the prerequisite libraries are loaded
+DO '1' LANGUAGE plpython2u;
+SELECT NULL::ltree;
+
+
+CREATE FUNCTION ltree_to_plpython2(val internal) RETURNS internal
+LANGUAGE C STRICT IMMUTABLE
+AS 'MODULE_PATHNAME', 'ltree_to_plpython';
+
+CREATE TRANSFORM FOR ltree LANGUAGE plpython2u (
+    FROM SQL WITH FUNCTION ltree_to_plpython2(internal)
+);
diff --git a/contrib/ltree_plpython/ltree_plpython2u.control b/contrib/ltree_plpython/ltree_plpython2u.control
new file mode 100644
index 0000000..bedfd0a
--- /dev/null
+++ b/contrib/ltree_plpython/ltree_plpython2u.control
@@ -0,0 +1,6 @@
+# ltree_plpython2u extension
+comment = 'transform between ltree and plpython2u'
+default_version = '1.0'
+module_pathname = '$libdir/ltree_plpython2'
+relocatable = true
+requires = 'ltree,plpython2u'
diff --git a/contrib/ltree_plpython/ltree_plpython3u--1.0.sql b/contrib/ltree_plpython/ltree_plpython3u--1.0.sql
new file mode 100644
index 0000000..1300a78
--- /dev/null
+++ b/contrib/ltree_plpython/ltree_plpython3u--1.0.sql
@@ -0,0 +1,12 @@
+-- make sure the prerequisite libraries are loaded
+DO '1' LANGUAGE plpython3u;
+SELECT NULL::ltree;
+
+
+CREATE FUNCTION ltree_to_plpython3(val internal) RETURNS internal
+LANGUAGE C STRICT IMMUTABLE
+AS 'MODULE_PATHNAME', 'ltree_to_plpython';
+
+CREATE TRANSFORM FOR ltree LANGUAGE plpython3u (
+    FROM SQL WITH FUNCTION ltree_to_plpython3(internal)
+);
diff --git a/contrib/ltree_plpython/ltree_plpython3u.control b/contrib/ltree_plpython/ltree_plpython3u.control
new file mode 100644
index 0000000..96c9764
--- /dev/null
+++ b/contrib/ltree_plpython/ltree_plpython3u.control
@@ -0,0 +1,6 @@
+# ltree_plpython3u extension
+comment = 'transform between ltree and plpython3u'
+default_version = '1.0'
+module_pathname = '$libdir/ltree_plpython3'
+relocatable = true
+requires = 'ltree,plpython3u'
diff --git a/contrib/ltree_plpython/ltree_plpythonu--1.0.sql b/contrib/ltree_plpython/ltree_plpythonu--1.0.sql
new file mode 100644
index 0000000..1d1af28
--- /dev/null
+++ b/contrib/ltree_plpython/ltree_plpythonu--1.0.sql
@@ -0,0 +1,12 @@
+-- make sure the prerequisite libraries are loaded
+DO '1' LANGUAGE plpythonu;
+SELECT NULL::ltree;
+
+
+CREATE FUNCTION ltree_to_plpython(val internal) RETURNS internal
+LANGUAGE C STRICT IMMUTABLE
+AS 'MODULE_PATHNAME';
+
+CREATE TRANSFORM FOR ltree LANGUAGE plpythonu (
+    FROM SQL WITH FUNCTION ltree_to_plpython(internal)
+);
diff --git a/contrib/ltree_plpython/ltree_plpythonu.control b/contrib/ltree_plpython/ltree_plpythonu.control
new file mode 100644
index 0000000..b03c89a
--- /dev/null
+++ b/contrib/ltree_plpython/ltree_plpythonu.control
@@ -0,0 +1,6 @@
+# ltree_plpythonu extension
+comment = 'transform between ltree and plpythonu'
+default_version = '1.0'
+module_pathname = '$libdir/ltree_plpython2'
+relocatable = true
+requires = 'ltree,plpythonu'
diff --git a/contrib/ltree_plpython/sql/ltree_plpython.sql b/contrib/ltree_plpython/sql/ltree_plpython.sql
new file mode 100644
index 0000000..2785592
--- /dev/null
+++ b/contrib/ltree_plpython/sql/ltree_plpython.sql
@@ -0,0 +1,34 @@
+CREATE EXTENSION plpython2u;
+CREATE EXTENSION ltree_plpython2u;
+
+
+CREATE FUNCTION test1(val ltree) RETURNS int
+LANGUAGE plpythonu
+AS $$
+plpy.info(repr(val))
+return len(val)
+$$;
+
+SELECT test1('aa.bb.cc'::ltree);
+
+
+CREATE FUNCTION test1n(val ltree) RETURNS int
+LANGUAGE plpython2u
+AS $$
+plpy.info(repr(val))
+return len(val)
+$$;
+
+SELECT test1n('aa.bb.cc'::ltree);
+
+
+CREATE FUNCTION test2() RETURNS ltree
+LANGUAGE plpythonu
+AS $$
+return ['foo', 'bar', 'baz']
+$$;
+
+-- plpython to ltree is not yet implemented, so this will fail,
+-- because it will try to parse the Python list as an ltree input
+-- string.
+SELECT test2();
diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index e638a8f..c4b3b42 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -264,6 +264,11 @@ <title>System Catalogs</title>
      </row>

      <row>
+      <entry><link linkend="catalog-pg-transform"><structname>pg_transform</structname></link></entry>
+      <entry>transforms (data type to procedural language conversions)</entry>
+     </row>
+
+     <row>
       <entry><link linkend="catalog-pg-trigger"><structname>pg_trigger</structname></link></entry>
       <entry>triggers</entry>
      </row>
@@ -5722,6 +5727,74 @@ <title><structname>pg_tablespace</> Columns</title>
  </sect1>


+ <sect1 id="catalog-pg-transform">
+  <title><structname>pg_transform</structname></title>
+
+  <indexterm zone="catalog-pg-transform">
+   <primary>pg_transform</primary>
+  </indexterm>
+
+  <para>
+   The catalog <structname>pg_transform</structname> stores information about
+   transforms, which are a mechanism to adapt data types to procedural
+   languages.  See <xref linkend="sql-createtransform"> for more information.
+  </para>
+
+  <table>
+   <title><structname>pg_transform</> Columns</title>
+
+   <tgroup cols="4">
+    <thead>
+     <row>
+      <entry>Name</entry>
+      <entry>Type</entry>
+      <entry>References</entry>
+      <entry>Description</entry>
+     </row>
+    </thead>
+
+    <tbody>
+     <row>
+      <entry><structfield>trftype</structfield></entry>
+      <entry><type>oid</type></entry>
+      <entry><literal><link linkend="catalog-pg-type"><structname>pg_type</structname></link>.oid</literal></entry>
+      <entry>OID of the data type this transform is for</entry>
+     </row>
+
+     <row>
+      <entry><structfield>trflang</structfield></entry>
+      <entry><type>oid</type></entry>
+      <entry><literal><link
linkend="catalog-pg-language"><structname>pg_language</structname></link>.oid</literal></entry>
+      <entry>OID of the language this transform is for</entry>
+     </row>
+
+     <row>
+      <entry><structfield>trffromsql</structfield></entry>
+      <entry><type>regproc</type></entry>
+      <entry><literal><link linkend="catalog-pg-proc"><structname>pg_proc</structname></link>.oid</literal></entry>
+      <entry>
+       The OID of the function to use when converting the data type for input
+       to the procedural language (e.g., function parameters).  Zero is stored
+       if this operation is not supported.
+      </entry>
+     </row>
+
+     <row>
+      <entry><structfield>trftosql</structfield></entry>
+      <entry><type>regproc</type></entry>
+      <entry><literal><link linkend="catalog-pg-proc"><structname>pg_proc</structname></link>.oid</literal></entry>
+      <entry>
+       The OID of the function to use when converting output from the
+       procedural language (e.g., return values) to the data type.  Zero is
+       stored if this operation is not supported.
+      </entry>
+     </row>
+    </tbody>
+   </tgroup>
+  </table>
+ </sect1>
+
+
  <sect1 id="catalog-pg-trigger">
   <title><structname>pg_trigger</structname></title>

diff --git a/doc/src/sgml/hstore.sgml b/doc/src/sgml/hstore.sgml
index 73c421d..a8f1537 100644
--- a/doc/src/sgml/hstore.sgml
+++ b/doc/src/sgml/hstore.sgml
@@ -597,6 +597,24 @@ <title>Compatibility</title>
  </sect2>

  <sect2>
+  <title>Transforms</title>
+
+  <para>
+   Additional extensions are available that implement transforms for
+   the <type>hstore</type> type for the languages PL/Perl and PL/Python.  The
+   extensions for PL/Perl are called <literal>hstore_plperl</literal>
+   and <literal>hstore_plperlu</literal>, for trusted and untrusted PL/Perl.
+   If you install these extensions, <type>hstore</type> values are
+   automatically mapped to Perl hashes.  The extensions for PL/Python are
+   called <literal>hstore_plpythonu</literal>, <literal>hstore_plpython2u</literal>,
+   and <literal>hstore_plpython3u</literal>
+   (see <xref linkend="plpython-python23"> for the PL/Python naming
+   convention).  If you install them, <type>hstore</type> values are
+   automatically mapped to Python dictionaries.
+  </para>
+ </sect2>
+
+ <sect2>
   <title>Authors</title>

   <para>
diff --git a/doc/src/sgml/ltree.sgml b/doc/src/sgml/ltree.sgml
index f5a0ac9..9f80d75 100644
--- a/doc/src/sgml/ltree.sgml
+++ b/doc/src/sgml/ltree.sgml
@@ -665,6 +665,21 @@ <title>Example</title>
  </sect2>

  <sect2>
+  <title>Transforms</title>
+
+  <para>
+   Additional extensions are available that implement transforms for
+   the <type>ltree</type> type for PL/Python.  The extensions are
+   called <literal>ltree_plpythonu</literal>, <literal>ltree_plpython2u</literal>,
+   and <literal>ltree_plpython3u</literal>
+   (see <xref linkend="plpython-python23"> for the PL/Python naming
+   convention).  If you install them, <type>ltree</type> values are
+   automatically mapped to Python lists.  (The reverse is currently not
+   supported, however.)
+  </para>
+ </sect2>
+
+ <sect2>
   <title>Authors</title>

   <para>
diff --git a/doc/src/sgml/ref/allfiles.sgml b/doc/src/sgml/ref/allfiles.sgml
index 5846974..9ee8517 100644
--- a/doc/src/sgml/ref/allfiles.sgml
+++ b/doc/src/sgml/ref/allfiles.sgml
@@ -76,6 +76,7 @@
 <!ENTITY createTable        SYSTEM "create_table.sgml">
 <!ENTITY createTableAs      SYSTEM "create_table_as.sgml">
 <!ENTITY createTableSpace   SYSTEM "create_tablespace.sgml">
+<!ENTITY createTransform    SYSTEM "create_transform.sgml">
 <!ENTITY createTrigger      SYSTEM "create_trigger.sgml">
 <!ENTITY createTSConfig     SYSTEM "create_tsconfig.sgml">
 <!ENTITY createTSDictionary SYSTEM "create_tsdictionary.sgml">
@@ -116,6 +117,7 @@
 <!ENTITY dropServer         SYSTEM "drop_server.sgml">
 <!ENTITY dropTable          SYSTEM "drop_table.sgml">
 <!ENTITY dropTableSpace     SYSTEM "drop_tablespace.sgml">
+<!ENTITY dropTransform      SYSTEM "drop_transform.sgml">
 <!ENTITY dropTrigger        SYSTEM "drop_trigger.sgml">
 <!ENTITY dropTSConfig       SYSTEM "drop_tsconfig.sgml">
 <!ENTITY dropTSDictionary   SYSTEM "drop_tsdictionary.sgml">
diff --git a/doc/src/sgml/ref/alter_extension.sgml b/doc/src/sgml/ref/alter_extension.sgml
index 2dbba0c..f0fad1e 100644
--- a/doc/src/sgml/ref/alter_extension.sgml
+++ b/doc/src/sgml/ref/alter_extension.sgml
@@ -52,6 +52,7 @@
   TEXT SEARCH DICTIONARY <replaceable class="PARAMETER">object_name</replaceable> |
   TEXT SEARCH PARSER <replaceable class="PARAMETER">object_name</replaceable> |
   TEXT SEARCH TEMPLATE <replaceable class="PARAMETER">object_name</replaceable> |
+  TRANSFORM FOR <replaceable>type_name</replaceable> LANGUAGE <replaceable>lang_name</replaceable> |
   TYPE <replaceable class="PARAMETER">object_name</replaceable> |
   VIEW <replaceable class="PARAMETER">object_name</replaceable>
 </synopsis>
@@ -264,6 +265,26 @@ <title>Parameters</title>
       </para>
      </listitem>
     </varlistentry>
+
+    <varlistentry>
+     <term><replaceable>type_name</replaceable></term>
+
+     <listitem>
+      <para>
+       The name of the data type of the transform.
+      </para>
+     </listitem>
+    </varlistentry>
+
+    <varlistentry>
+     <term><replaceable>lang_name</replaceable></term>
+
+     <listitem>
+      <para>
+       The name of the language of the transform.
+      </para>
+     </listitem>
+    </varlistentry>
    </variablelist>
   </para>
  </refsect1>
diff --git a/doc/src/sgml/ref/comment.sgml b/doc/src/sgml/ref/comment.sgml
index e94dd4b..7aad302 100644
--- a/doc/src/sgml/ref/comment.sgml
+++ b/doc/src/sgml/ref/comment.sgml
@@ -54,6 +54,7 @@
   TEXT SEARCH DICTIONARY <replaceable class="PARAMETER">object_name</replaceable> |
   TEXT SEARCH PARSER <replaceable class="PARAMETER">object_name</replaceable> |
   TEXT SEARCH TEMPLATE <replaceable class="PARAMETER">object_name</replaceable> |
+  TRANSFORM FOR <replaceable>type_name</replaceable> LANGUAGE <replaceable>lang_name</replaceable> |
   TRIGGER <replaceable class="PARAMETER">trigger_name</replaceable> ON <replaceable
class="PARAMETER">table_name</replaceable>| 
   TYPE <replaceable class="PARAMETER">object_name</replaceable> |
   VIEW <replaceable class="PARAMETER">object_name</replaceable>
@@ -217,6 +218,26 @@ <title>Parameters</title>
      </listitem>
     </varlistentry>

+    <varlistentry>
+     <term><replaceable>type_name</replaceable></term>
+
+     <listitem>
+      <para>
+       The name of the data type of the transform.
+      </para>
+     </listitem>
+    </varlistentry>
+
+    <varlistentry>
+     <term><replaceable>lang_name</replaceable></term>
+
+     <listitem>
+      <para>
+       The name of the language of the transform.
+      </para>
+     </listitem>
+    </varlistentry>
+
    <varlistentry>
     <term><replaceable class="parameter">text</replaceable></term>
     <listitem>
@@ -296,6 +317,7 @@ <title>Examples</title>
 COMMENT ON TEXT SEARCH DICTIONARY swedish IS 'Snowball stemmer for swedish language';
 COMMENT ON TEXT SEARCH PARSER my_parser IS 'Splits text into words';
 COMMENT ON TEXT SEARCH TEMPLATE snowball IS 'Snowball stemmer';
+COMMENT ON TRANSFORM FOR hstore LANGUAGE plpythonu IS 'Transform between hstore and Python dict';
 COMMENT ON TRIGGER my_trigger ON my_table IS 'Used for RI';
 COMMENT ON TYPE complex IS 'Complex number data type';
 COMMENT ON VIEW my_view IS 'View of departmental costs';
diff --git a/doc/src/sgml/ref/create_transform.sgml b/doc/src/sgml/ref/create_transform.sgml
new file mode 100644
index 0000000..2f10cee
--- /dev/null
+++ b/doc/src/sgml/ref/create_transform.sgml
@@ -0,0 +1,187 @@
+<!-- doc/src/sgml/ref/create_transform.sgml -->
+
+<refentry id="SQL-CREATETRANSFORM">
+ <refmeta>
+  <refentrytitle>CREATE TRANSFORM</refentrytitle>
+  <manvolnum>7</manvolnum>
+  <refmiscinfo>SQL - Language Statements</refmiscinfo>
+ </refmeta>
+
+ <refnamediv>
+  <refname>CREATE TRANSFORM</refname>
+  <refpurpose>define a new transform</refpurpose>
+ </refnamediv>
+
+ <indexterm zone="sql-createtransform">
+  <primary>CREATE TRANSFORM</primary>
+ </indexterm>
+
+ <refsynopsisdiv>
+<synopsis>
+CREATE [ OR REPLACE ] TRANSFORM FOR <replaceable>type_name</replaceable> LANGUAGE <replaceable>lang_name</replaceable>
(
+    FROM SQL WITH FUNCTION <replaceable>from_sql_function_name</replaceable> (<replaceable>argument_type</replaceable>
[,...]), 
+    TO SQL WITH FUNCTION <replaceable>to_sql_function_name</replaceable> (<replaceable>argument_type</replaceable> [,
...])
+);
+</synopsis>
+ </refsynopsisdiv>
+
+ <refsect1 id="sql-createtransform-description">
+  <title>Description</title>
+
+  <para>
+   <command>CREATE TRANSFORM</command> defines a new transform.
+   <command>CREATE OR REPLACE TRANSFORM</command> will either create a new
+   transform, or replace an existing definition.
+  </para>
+
+  <para>
+   A transform specifies how to adapt a data type to a procedural language.
+   For example, when writing a function in PL/Python using the hstore type,
+   PL/Python has no prior knowledge how to present hstore values in the Python
+   environment.  Language implementations usually default to using the text
+   representation, but that is inconvenient when, for example, an associative
+   array or a list would be more appropriate.  A transform specifies two
+   functions: one <quote>from SQL</quote> function that converts the type from
+   the SQL environment to the language (In other words, this function will be
+   invoked on the arguments of a function written in the language.), and one
+   <quote>to SQL</quote> function that converts the type from the language to
+   the SQL environment (In other words, this function will be invoked on the
+   return value of a function written in the language.).  It is not necessary
+   to provide both of these functions.  If one is not specified, the
+   language-specific default behavior will be used if necessary.  (To prevent a
+   transformation in a certain direction from happening at all, you could also
+   write a transform function that always errors out.)
+  </para>
+
+  <para>
+   To be able to create a transform, you must own the type and have
+   <literal>USAGE</literal> privilege on both the type and the language.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>Parameters</title>
+
+   <variablelist>
+    <varlistentry>
+     <term><replaceable>type_name</replaceable></term>
+
+     <listitem>
+      <para>
+       The name of the data type of the transform.
+      </para>
+     </listitem>
+    </varlistentry>
+
+    <varlistentry>
+     <term><replaceable>lang_name</replaceable></term>
+
+     <listitem>
+      <para>
+       The name of the language of the transform.
+      </para>
+     </listitem>
+    </varlistentry>
+
+    <varlistentry>
+     <term><replaceable>from_sql_function_name</replaceable>(<replaceable>argument_type</replaceable> [, ...])</term>
+
+     <listitem>
+      <para>
+       The name of the function for converting the type from the SQL
+       environment to the language.  It must take one argument of
+       type <type>internal</type> and return type <type>internal</type>.  The
+       actual argument will be of the type for the transform, and the function
+       should be coded as if it were, but it is not allowed to declare an
+       SQL-level function function returning <type>internal</type> without at
+       least one argument of type <type>internal</type>.  The actual return
+       value will be something specific to the language implementation.
+      </para>
+     </listitem>
+    </varlistentry>
+
+    <varlistentry>
+     <term><replaceable>to_sql_function_name</replaceable>(<replaceable>argument_type</replaceable> [, ...])</term>
+
+     <listitem>
+      <para>
+       The name of the function for converting the type from the language to
+       the SQL environment.  It must take one argument of type
+       <type>internal</type> and return the type that is the type for the
+       transform.  The actual argument value will be something specific to the
+       language implementation.
+      </para>
+     </listitem>
+    </varlistentry>
+   </variablelist>
+ </refsect1>
+
+ <refsect1 id="sql-createtransform-notes">
+  <title>Notes</title>
+
+  <para>
+   Use <xref linkend="sql-droptransform"> to remove transforms.
+  </para>
+ </refsect1>
+
+ <refsect1 id="sql-createtransform-examples">
+  <title>Examples</title>
+
+  <para>
+   To create a transform for type <type>hstore</type> and language
+   <literal>plpythonu</literal>, first set up the type and the language:
+<programlisting>
+CREATE TYPE hstore ...;
+
+CREATE LANGUAGE plpythonu ...;
+</programlisting>
+   Then create the necessary functions:
+<programlisting>
+CREATE FUNCTION hstore_to_plpython(val internal) RETURNS internal
+LANGUAGE C STRICT IMMUTABLE
+AS ...;
+
+CREATE FUNCTION plpython_to_hstore(val internal) RETURNS hstore
+LANGUAGE C STRICT IMMUTABLE
+AS ...;
+</programlisting>
+   And finally create the transform to connect them all together:
+<programlisting>
+CREATE TRANSFORM FOR hstore LANGUAGE plpythonu (
+    FROM SQL WITH FUNCTION hstore_to_plpython(internal),
+    TO SQL WITH FUNCTION plpython_to_hstore(internal)
+);
+</programlisting>
+   In practice, these commands would be wrapped up in extensions.
+  </para>
+
+  <para>
+   The <filename>contrib</filename> section contains a number of extensions
+   that provide transforms, which can serve as real-world examples.
+  </para>
+ </refsect1>
+
+ <refsect1 id="sql-createtransform-compat">
+  <title>Compatibility</title>
+
+  <para>
+   This form of <command>CREATE TRANSFORM</command> is a
+   <productname>PostgreSQL</productname> extension.  There is a <command>CREATE
+   TRANSFORM</command> command in the <acronym>SQL</acronym> standard, but it
+   is for adapting data types to client languages.  That usage is not supported
+   by <productname>PostgreSQL</productname>.
+  </para>
+ </refsect1>
+
+ <refsect1 id="sql-createtransform-seealso">
+  <title>See Also</title>
+
+  <para>
+   <xref linkend="sql-createfunction">,
+   <xref linkend="sql-createlanguage">,
+   <xref linkend="sql-createtype">,
+   <xref linkend="sql-droptransform">
+  </para>
+ </refsect1>
+
+</refentry>
diff --git a/doc/src/sgml/ref/drop_transform.sgml b/doc/src/sgml/ref/drop_transform.sgml
new file mode 100644
index 0000000..c9b558e
--- /dev/null
+++ b/doc/src/sgml/ref/drop_transform.sgml
@@ -0,0 +1,123 @@
+<!-- doc/src/sgml/ref/drop_transform.sgml -->
+
+<refentry id="SQL-DROPTRANSFORM">
+ <refmeta>
+  <refentrytitle>DROP TRANSFORM</refentrytitle>
+  <manvolnum>7</manvolnum>
+  <refmiscinfo>SQL - Language Statements</refmiscinfo>
+ </refmeta>
+
+ <refnamediv>
+  <refname>DROP TRANSFORM</refname>
+  <refpurpose>remove a transform</refpurpose>
+ </refnamediv>
+
+ <indexterm zone="sql-droptransform">
+  <primary>DROP TRANSFORM</primary>
+ </indexterm>
+
+ <refsynopsisdiv>
+<synopsis>
+DROP TRANSFORM [ IF EXISTS ] FOR <replaceable>type_name</replaceable> LANGUAGE <replaceable>lang_name</replaceable>
+</synopsis>
+ </refsynopsisdiv>
+
+ <refsect1 id="sql-droptransform-description">
+  <title>Description</title>
+
+  <para>
+   <command>DROP TRANSFORM</command> removes a previously defined transform.
+  </para>
+
+  <para>
+   To be able to drop a transform, you must own the type and the language.
+   These are the same privileges that are required to create a transform.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>Parameters</title>
+
+   <variablelist>
+
+   <varlistentry>
+    <term><literal>IF EXISTS</literal></term>
+    <listitem>
+     <para>
+      Do not throw an error if the transform does not exist. A notice is issued
+      in this case.
+     </para>
+    </listitem>
+   </varlistentry>
+
+    <varlistentry>
+     <term><replaceable>type_name</replaceable></term>
+
+     <listitem>
+      <para>
+       The name of the data type of the transform.
+      </para>
+     </listitem>
+    </varlistentry>
+
+    <varlistentry>
+     <term><replaceable>lang_name</replaceable></term>
+
+     <listitem>
+      <para>
+       The name of the language of the transform.
+      </para>
+     </listitem>
+    </varlistentry>
+
+    <varlistentry>
+     <term><literal>CASCADE</literal></term>
+     <listitem>
+      <para>
+       Automatically drop objects that depend on the transform.
+      </para>
+     </listitem>
+    </varlistentry>
+
+    <varlistentry>
+     <term><literal>RESTRICT</literal></term>
+     <listitem>
+      <para>
+       Refuse to drop the transform if any objects depend on it.  This is the
+       default.
+      </para>
+     </listitem>
+    </varlistentry>
+   </variablelist>
+ </refsect1>
+
+ <refsect1 id="sql-droptransform-examples">
+  <title>Examples</title>
+
+  <para>
+   To drop the transform for type <type>hstore</type> and language
+   <literal>plpythonu</literal>:
+<programlisting>
+DROP TRANSFORM FOR hstore LANGUAGE plpythonu;
+</programlisting></para>
+ </refsect1>
+
+ <refsect1 id="sql-droptransform-compat">
+  <title>Compatibility</title>
+
+  <para>
+   This form of <command>DROP TRANSFORM</command> is a
+   <productname>PostgreSQL</productname> extension.  See <xref
+   linkend="sql-createtransform"> for details.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>See Also</title>
+
+  <simplelist type="inline">
+   <member><xref linkend="sql-createtransform"></member>
+  </simplelist>
+ </refsect1>
+
+</refentry>
diff --git a/doc/src/sgml/reference.sgml b/doc/src/sgml/reference.sgml
index 14e217a..0aac876 100644
--- a/doc/src/sgml/reference.sgml
+++ b/doc/src/sgml/reference.sgml
@@ -108,6 +108,7 @@ <title>SQL Commands</title>
    &createTSDictionary;
    &createTSParser;
    &createTSTemplate;
+   &createTransform;
    &createTrigger;
    &createType;
    &createUser;
@@ -148,6 +149,7 @@ <title>SQL Commands</title>
    &dropTSDictionary;
    &dropTSParser;
    &dropTSTemplate;
+   &dropTransform;
    &dropTrigger;
    &dropType;
    &dropUser;
diff --git a/src/Makefile.shlib b/src/Makefile.shlib
index 2a0c7a9..3ab9b82 100644
--- a/src/Makefile.shlib
+++ b/src/Makefile.shlib
@@ -133,7 +133,7 @@ ifeq ($(PORTNAME), darwin)
   else
     # loadable module
     DLSUFFIX        = .so
-    LINK.shared        = $(COMPILER) -bundle -multiply_defined suppress
+    LINK.shared        = $(COMPILER) -bundle -multiply_defined suppress -Wl,-undefined,dynamic_lookup
   endif
   BUILD.exports        = $(AWK) '/^[^\#]/ {printf "_%s\n",$$1}' $< >$@
   exports_file        = $(SHLIB_EXPORTS:%.txt=%.list)
diff --git a/src/backend/catalog/Makefile b/src/backend/catalog/Makefile
index c4d3f3c..f02ea60 100644
--- a/src/backend/catalog/Makefile
+++ b/src/backend/catalog/Makefile
@@ -41,6 +41,7 @@ POSTGRES_BKI_SRCS = $(addprefix $(top_srcdir)/src/include/catalog/,\
     pg_foreign_data_wrapper.h pg_foreign_server.h pg_user_mapping.h \
     pg_foreign_table.h \
     pg_default_acl.h pg_seclabel.h pg_shseclabel.h pg_collation.h pg_range.h \
+    pg_transform.h \
     toasting.h indexing.h \
     )

diff --git a/src/backend/catalog/dependency.c b/src/backend/catalog/dependency.c
index 69171f8..52aae48 100644
--- a/src/backend/catalog/dependency.c
+++ b/src/backend/catalog/dependency.c
@@ -46,6 +46,7 @@
 #include "catalog/pg_proc.h"
 #include "catalog/pg_rewrite.h"
 #include "catalog/pg_tablespace.h"
+#include "catalog/pg_transform.h"
 #include "catalog/pg_trigger.h"
 #include "catalog/pg_ts_config.h"
 #include "catalog/pg_ts_dict.h"
@@ -1249,6 +1250,10 @@ static bool stack_address_present_add_flags(const ObjectAddress *object,
             RemoveEventTriggerById(object->objectId);
             break;

+        case OCLASS_TRANSFORM:
+            DropTransformById(object->objectId);
+            break;
+
         default:
             elog(ERROR, "unrecognized object class: %u",
                  object->classId);
@@ -2308,6 +2313,9 @@ static bool stack_address_present_add_flags(const ObjectAddress *object,

         case EventTriggerRelationId:
             return OCLASS_EVENT_TRIGGER;
+
+        case TransformRelationId:
+            return OCLASS_TRANSFORM;
     }

     /* shouldn't get here */
diff --git a/src/backend/catalog/objectaddress.c b/src/backend/catalog/objectaddress.c
index 215eaf5..dd267c1 100644
--- a/src/backend/catalog/objectaddress.c
+++ b/src/backend/catalog/objectaddress.c
@@ -44,6 +44,7 @@
 #include "catalog/pg_proc.h"
 #include "catalog/pg_rewrite.h"
 #include "catalog/pg_tablespace.h"
+#include "catalog/pg_transform.h"
 #include "catalog/pg_trigger.h"
 #include "catalog/pg_ts_config.h"
 #include "catalog/pg_ts_dict.h"
@@ -332,6 +333,12 @@
         true
     },
     {
+        TransformRelationId,
+        TransformOidIndexId,
+        TRFOID,
+        InvalidAttrNumber
+    },
+    {
         TriggerRelationId,
         TriggerOidIndexId,
         -1,
@@ -589,6 +596,29 @@ static void getRelationTypeDescription(StringInfo buffer, Oid relid,
                     address.objectSubId = 0;
                 }
                 break;
+            case OBJECT_TRANSFORM:
+                {
+                    TypeName   *typename = (TypeName *) linitial(objname);
+                    char       *langname = (char *) linitial(objargs);
+                    Oid            typeid = typenameTypeId(NULL, typename);
+                    Oid            langid;
+                    HeapTuple    tuple;
+
+                    tuple = SearchSysCache1(LANGNAME, PointerGetDatum(langname));
+                    if (!HeapTupleIsValid(tuple))
+                        ereport(ERROR,
+                                (errcode(ERRCODE_UNDEFINED_OBJECT),
+                                 errmsg("language \"%s\" does not exist", langname)));
+
+                    langid = HeapTupleGetOid(tuple);
+                    ReleaseSysCache(tuple);
+
+                    address.classId = TransformRelationId;
+                    address.objectId =
+                        get_transform_oid(typeid, langid, missing_ok);
+                    address.objectSubId = 0;
+                }
+                break;
             case OBJECT_TSPARSER:
                 address.classId = TSParserRelationId;
                 address.objectId = get_ts_parser_oid(objname, missing_ok);
@@ -1234,6 +1264,16 @@ static void getRelationTypeDescription(StringInfo buffer, Oid relid,
                                     format_type_be(targettypeid))));
             }
             break;
+        case OBJECT_TRANSFORM:
+            {
+                TypeName   *typename = (TypeName *) linitial(objname);
+                Oid            typeid = typenameTypeId(NULL, typename);
+
+                if (!pg_type_ownercheck(typeid, roleid))
+                    aclcheck_error(ACLCHECK_NOT_OWNER, ACL_KIND_TYPE,
+                                   format_type_be(typeid));
+            }
+            break;
         case OBJECT_TABLESPACE:
             if (!pg_tablespace_ownercheck(address.objectId, roleid))
                 aclcheck_error(ACLCHECK_NOT_OWNER, ACL_KIND_TABLESPACE,
@@ -1667,19 +1707,10 @@ static void getRelationTypeDescription(StringInfo buffer, Oid relid,
             }

         case OCLASS_LANGUAGE:
-            {
-                HeapTuple    langTup;
+            appendStringInfo(&buffer, _("language %s"),
+                             get_language_name(object->objectId, false));
+            break;

-                langTup = SearchSysCache1(LANGOID,
-                                          ObjectIdGetDatum(object->objectId));
-                if (!HeapTupleIsValid(langTup))
-                    elog(ERROR, "cache lookup failed for language %u",
-                         object->objectId);
-                appendStringInfo(&buffer, _("language %s"),
-                  NameStr(((Form_pg_language) GETSTRUCT(langTup))->lanname));
-                ReleaseSysCache(langTup);
-                break;
-            }
         case OCLASS_LARGEOBJECT:
             appendStringInfo(&buffer, _("large object %u"),
                              object->objectId);
@@ -1867,6 +1898,27 @@ static void getRelationTypeDescription(StringInfo buffer, Oid relid,
                 break;
             }

+        case OCLASS_TRANSFORM:
+            {
+                HeapTuple    trfTup;
+                Form_pg_transform trfForm;
+
+                trfTup = SearchSysCache1(TRFOID,
+                                          ObjectIdGetDatum(object->objectId));
+                if (!HeapTupleIsValid(trfTup))
+                    elog(ERROR, "could not find tuple for transform %u",
+                         object->objectId);
+
+                trfForm = (Form_pg_transform) GETSTRUCT(trfTup);
+
+                appendStringInfo(&buffer, _("transform for %s language %s"),
+                                 format_type_be(trfForm->trftype),
+                                 get_language_name(trfForm->trflang, false));
+
+                ReleaseSysCache(trfTup);
+                break;
+            }
+
         case OCLASS_TRIGGER:
             {
                 Relation    trigDesc;
diff --git a/src/backend/catalog/pg_proc.c b/src/backend/catalog/pg_proc.c
index 2a98ca9..96aabb1 100644
--- a/src/backend/catalog/pg_proc.c
+++ b/src/backend/catalog/pg_proc.c
@@ -23,6 +23,7 @@
 #include "catalog/pg_namespace.h"
 #include "catalog/pg_proc.h"
 #include "catalog/pg_proc_fn.h"
+#include "catalog/pg_transform.h"
 #include "catalog/pg_type.h"
 #include "executor/functions.h"
 #include "funcapi.h"
@@ -116,6 +117,7 @@ static bool match_prosrc_to_literal(const char *prosrc, const char *literal,
     ObjectAddress myself,
                 referenced;
     int            i;
+    Oid            trfid;

     /*
      * sanity checks
@@ -624,6 +626,15 @@ static bool match_prosrc_to_literal(const char *prosrc, const char *literal,
     referenced.objectSubId = 0;
     recordDependencyOn(&myself, &referenced, DEPENDENCY_NORMAL);

+    /* dependency on transform used by return type, if any */
+    if ((trfid = get_transform(returnType, languageObjectId)))
+    {
+        referenced.classId = TransformRelationId;
+        referenced.objectId = trfid;
+        referenced.objectSubId = 0;
+        recordDependencyOn(&myself, &referenced, DEPENDENCY_NORMAL);
+    }
+
     /* dependency on parameter types */
     for (i = 0; i < allParamCount; i++)
     {
@@ -631,6 +642,15 @@ static bool match_prosrc_to_literal(const char *prosrc, const char *literal,
         referenced.objectId = allParams[i];
         referenced.objectSubId = 0;
         recordDependencyOn(&myself, &referenced, DEPENDENCY_NORMAL);
+
+        /* dependency on transform used by parameter type, if any */
+        if ((trfid = get_transform(allParams[i], languageObjectId)))
+        {
+            referenced.classId = TransformRelationId;
+            referenced.objectId = trfid;
+            referenced.objectSubId = 0;
+            recordDependencyOn(&myself, &referenced, DEPENDENCY_NORMAL);
+        }
     }

     /* dependency on parameter default expressions */
diff --git a/src/backend/commands/dropcmds.c b/src/backend/commands/dropcmds.c
index b32ad3a..c63e07b 100644
--- a/src/backend/commands/dropcmds.c
+++ b/src/backend/commands/dropcmds.c
@@ -200,6 +200,12 @@ static void does_not_exist_skipping(ObjectType objtype,
             args = format_type_be(typenameTypeId(NULL,
                                             (TypeName *) linitial(objargs)));
             break;
+        case OBJECT_TRANSFORM:
+            msg = gettext_noop("transform for type %s language %s does not exist, skipping");
+            name = format_type_be(typenameTypeId(NULL,
+                                  (TypeName *) linitial(objname)));
+            args = (char *) linitial(objargs);
+            break;
         case OBJECT_TRIGGER:
             msg = gettext_noop("trigger \"%s\" for table \"%s\" does not exist, skipping");
             name = strVal(llast(objname));
diff --git a/src/backend/commands/event_trigger.c b/src/backend/commands/event_trigger.c
index 328e2a8..7a53d56 100644
--- a/src/backend/commands/event_trigger.c
+++ b/src/backend/commands/event_trigger.c
@@ -92,6 +92,7 @@
     {"SERVER", true},
     {"TABLE", true},
     {"TABLESPACE", false},
+    {"TRANSFORM", true},
     {"TRIGGER", true},
     {"TEXT SEARCH CONFIGURATION", true},
     {"TEXT SEARCH DICTIONARY", true},
@@ -937,6 +938,7 @@ static Oid insert_event_trigger_tuple(char *trigname, char *eventname,
         case OBJECT_SCHEMA:
         case OBJECT_SEQUENCE:
         case OBJECT_TABLE:
+        case OBJECT_TRANSFORM:
         case OBJECT_TRIGGER:
         case OBJECT_TSCONFIGURATION:
         case OBJECT_TSDICTIONARY:
@@ -983,6 +985,7 @@ static Oid insert_event_trigger_tuple(char *trigname, char *eventname,
         case OCLASS_REWRITE:
         case OCLASS_TRIGGER:
         case OCLASS_SCHEMA:
+        case OCLASS_TRANSFORM:
         case OCLASS_TSPARSER:
         case OCLASS_TSDICT:
         case OCLASS_TSTEMPLATE:
diff --git a/src/backend/commands/functioncmds.c b/src/backend/commands/functioncmds.c
index c776758..17ba792 100644
--- a/src/backend/commands/functioncmds.c
+++ b/src/backend/commands/functioncmds.c
@@ -45,6 +45,7 @@
 #include "catalog/pg_namespace.h"
 #include "catalog/pg_proc.h"
 #include "catalog/pg_proc_fn.h"
+#include "catalog/pg_transform.h"
 #include "catalog/pg_type.h"
 #include "catalog/pg_type_fn.h"
 #include "commands/alter.h"
@@ -1618,6 +1619,298 @@
     heap_close(relation, RowExclusiveLock);
 }

+
+static void
+check_transform_function(Form_pg_proc procstruct)
+{
+    if (procstruct->provolatile == PROVOLATILE_VOLATILE)
+        ereport(ERROR,
+                (errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+                 errmsg("transform function must not be volatile")));
+    if (procstruct->proisagg)
+        ereport(ERROR,
+                (errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+                 errmsg("transform function must not be an aggregate function")));
+    if (procstruct->proiswindow)
+        ereport(ERROR,
+                (errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+                 errmsg("transform function must not be a window function")));
+    if (procstruct->proretset)
+        ereport(ERROR,
+                (errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+                 errmsg("transform function must not return a set")));
+    if (procstruct->pronargs != 1)
+        ereport(ERROR,
+                (errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+                 errmsg("transform function must take one argument")));
+    if (procstruct->proargtypes.values[0] != INTERNALOID)
+        ereport(ERROR,
+                (errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+                 errmsg("first argument of transform function must be type \"internal\"")));
+}
+
+
+/*
+ * CREATE TRANSFORM
+ */
+Oid
+CreateTransform(CreateTransformStmt *stmt)
+{
+    Oid            typeid;
+    char        typtype;
+    Oid            langid;
+    Oid            fromsqlfuncid;
+    Oid            tosqlfuncid;
+    AclResult    aclresult;
+    Form_pg_proc procstruct;
+    Datum        values[Natts_pg_transform];
+    bool        nulls[Natts_pg_transform];
+    bool        replaces[Natts_pg_transform];
+    Oid            transformid;
+    HeapTuple    tuple;
+    HeapTuple    newtuple;
+    Relation    relation;
+    ObjectAddress myself,
+                referenced;
+    bool        is_replace;
+
+    /*
+     * Get the type
+     */
+    typeid = typenameTypeId(NULL, stmt->type_name);
+    typtype = get_typtype(typeid);
+
+    if (typtype == TYPTYPE_PSEUDO)
+        ereport(ERROR,
+                (errcode(ERRCODE_WRONG_OBJECT_TYPE),
+                 errmsg("data type %s is a pseudo-type",
+                        TypeNameToString(stmt->type_name))));
+
+    if (typtype == TYPTYPE_DOMAIN)
+        ereport(ERROR,
+                (errcode(ERRCODE_WRONG_OBJECT_TYPE),
+                 errmsg("data type %s is a domain",
+                        TypeNameToString(stmt->type_name))));
+
+    if (!pg_type_ownercheck(typeid, GetUserId()))
+        ereport(ERROR,
+                (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+                 errmsg("must be owner of type %s",
+                        format_type_be(typeid))));
+
+    aclresult = pg_type_aclcheck(typeid, GetUserId(), ACL_USAGE);
+    if (aclresult != ACLCHECK_OK)
+        aclcheck_error(aclresult, ACL_KIND_TYPE,
+                       format_type_be(typeid));
+
+    /*
+     * Get the language
+     */
+    tuple = SearchSysCache1(LANGNAME, PointerGetDatum(stmt->lang));
+    if (!HeapTupleIsValid(tuple))
+        ereport(ERROR,
+                (errcode(ERRCODE_UNDEFINED_OBJECT),
+                 errmsg("language \"%s\" does not exist", stmt->lang)));
+
+    langid = HeapTupleGetOid(tuple);
+    ReleaseSysCache(tuple);
+
+    aclresult = pg_language_aclcheck(langid, GetUserId(), ACL_USAGE);
+    if (aclresult != ACLCHECK_OK)
+        aclcheck_error(aclresult, ACL_KIND_LANGUAGE, stmt->lang);
+
+    /*
+     * Get the functions
+     */
+    if (stmt->fromsql)
+    {
+        fromsqlfuncid = LookupFuncNameTypeNames(stmt->fromsql->funcname, stmt->fromsql->funcargs, false);
+
+        aclresult = pg_proc_aclcheck(fromsqlfuncid, GetUserId(), ACL_EXECUTE);
+        if (aclresult != ACLCHECK_OK)
+            aclcheck_error(aclresult, ACL_KIND_PROC, NameListToString(stmt->fromsql->funcname));
+
+        tuple = SearchSysCache1(PROCOID, ObjectIdGetDatum(fromsqlfuncid));
+        if (!HeapTupleIsValid(tuple))
+            elog(ERROR, "cache lookup failed for function %u", fromsqlfuncid);
+        procstruct = (Form_pg_proc) GETSTRUCT(tuple);
+        if (procstruct->prorettype != INTERNALOID)
+            ereport(ERROR,
+                    (errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+                     errmsg("return data type of FROM SQL function must be \"internal\"")));
+        check_transform_function(procstruct);
+        ReleaseSysCache(tuple);
+    }
+    else
+        fromsqlfuncid = InvalidOid;
+
+    if (stmt->tosql)
+    {
+        tosqlfuncid = LookupFuncNameTypeNames(stmt->tosql->funcname, stmt->tosql->funcargs, false);
+
+        aclresult = pg_proc_aclcheck(tosqlfuncid, GetUserId(), ACL_EXECUTE);
+        if (aclresult != ACLCHECK_OK)
+            aclcheck_error(aclresult, ACL_KIND_PROC, NameListToString(stmt->tosql->funcname));
+
+        tuple = SearchSysCache1(PROCOID, ObjectIdGetDatum(tosqlfuncid));
+        if (!HeapTupleIsValid(tuple))
+            elog(ERROR, "cache lookup failed for function %u", tosqlfuncid);
+        procstruct = (Form_pg_proc) GETSTRUCT(tuple);
+        if (procstruct->prorettype != typeid)
+            ereport(ERROR,
+                    (errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+                     errmsg("return data type of TO SQL function must be the transform data type")));
+        check_transform_function(procstruct);
+        ReleaseSysCache(tuple);
+    }
+    else
+        tosqlfuncid = InvalidOid;
+
+    /*
+     * Ready to go
+     */
+    values[Anum_pg_transform_trftype - 1] = ObjectIdGetDatum(typeid);
+    values[Anum_pg_transform_trflang - 1] = ObjectIdGetDatum(langid);
+    values[Anum_pg_transform_trffromsql - 1] = ObjectIdGetDatum(fromsqlfuncid);
+    values[Anum_pg_transform_trftosql - 1] = ObjectIdGetDatum(tosqlfuncid);
+
+    MemSet(nulls, false, sizeof(nulls));
+
+    relation = heap_open(TransformRelationId, RowExclusiveLock);
+
+    tuple = SearchSysCache2(TRFTYPELANG,
+                            ObjectIdGetDatum(typeid),
+                            ObjectIdGetDatum(langid));
+    if (HeapTupleIsValid(tuple))
+    {
+        if (!stmt->replace)
+            ereport(ERROR,
+                    (errcode(ERRCODE_DUPLICATE_OBJECT),
+                     errmsg("transform for type %s language %s already exists",
+                            format_type_be(typeid),
+                            stmt->lang)));
+
+        MemSet(replaces, false, sizeof(replaces));
+        replaces[Anum_pg_transform_trffromsql - 1] = true;
+        replaces[Anum_pg_transform_trftosql - 1] = true;
+
+        newtuple = heap_modify_tuple(tuple, RelationGetDescr(relation), values, nulls, replaces);
+        simple_heap_update(relation, &newtuple->t_self, newtuple);
+
+        transformid = HeapTupleGetOid(tuple);
+        ReleaseSysCache(tuple);
+        is_replace = true;
+    }
+    else
+    {
+        newtuple = heap_form_tuple(RelationGetDescr(relation), values, nulls);
+        transformid = simple_heap_insert(relation, newtuple);
+        is_replace = false;
+    }
+
+    CatalogUpdateIndexes(relation, newtuple);
+
+    if (is_replace)
+        deleteDependencyRecordsFor(TransformRelationId, transformid, true);
+
+    /* make dependency entries */
+    myself.classId = TransformRelationId;
+    myself.objectId = transformid;
+    myself.objectSubId = 0;
+
+    /* dependency on language */
+    referenced.classId = LanguageRelationId;
+    referenced.objectId = langid;
+    referenced.objectSubId = 0;
+    recordDependencyOn(&myself, &referenced, DEPENDENCY_NORMAL);
+
+    /* dependency on type */
+    referenced.classId = TypeRelationId;
+    referenced.objectId = typeid;
+    referenced.objectSubId = 0;
+    recordDependencyOn(&myself, &referenced, DEPENDENCY_NORMAL);
+
+    /* dependencies on functions */
+    if (OidIsValid(fromsqlfuncid))
+    {
+        referenced.classId = ProcedureRelationId;
+        referenced.objectId = fromsqlfuncid;
+        referenced.objectSubId = 0;
+        recordDependencyOn(&myself, &referenced, DEPENDENCY_NORMAL);
+    }
+    if (OidIsValid(tosqlfuncid))
+    {
+        referenced.classId = ProcedureRelationId;
+        referenced.objectId = tosqlfuncid;
+        referenced.objectSubId = 0;
+        recordDependencyOn(&myself, &referenced, DEPENDENCY_NORMAL);
+    }
+
+    /* dependency on extension */
+    recordDependencyOnCurrentExtension(&myself, is_replace);
+
+    /* Post creation hook for new transform */
+    InvokeObjectPostCreateHook(TransformRelationId, transformid, 0);
+
+    heap_freetuple(newtuple);
+
+    heap_close(relation, RowExclusiveLock);
+
+    return transformid;
+}
+
+
+/*
+ * get_transform_oid - given type OID and language OID, look up a transform OID
+ *
+ * If missing_ok is false, throw an error if the transform is not found.  If
+ * true, just return InvalidOid.
+ */
+Oid
+get_transform_oid(Oid typeid, Oid langid, bool missing_ok)
+{
+    Oid            oid;
+
+    oid = GetSysCacheOid2(TRFTYPELANG,
+                          ObjectIdGetDatum(typeid),
+                          ObjectIdGetDatum(langid));
+    if (!OidIsValid(oid) && !missing_ok)
+        ereport(ERROR,
+                (errcode(ERRCODE_UNDEFINED_OBJECT),
+                 errmsg("transform for type %s language \"%s\" does not exist",
+                        format_type_be(typeid),
+                        get_language_name(langid, false))));
+    return oid;
+}
+
+
+void
+DropTransformById(Oid transformOid)
+{
+    Relation    relation;
+    ScanKeyData scankey;
+    SysScanDesc scan;
+    HeapTuple    tuple;
+
+    relation = heap_open(TransformRelationId, RowExclusiveLock);
+
+    ScanKeyInit(&scankey,
+                ObjectIdAttributeNumber,
+                BTEqualStrategyNumber, F_OIDEQ,
+                ObjectIdGetDatum(transformOid));
+    scan = systable_beginscan(relation, TransformOidIndexId, true,
+                              SnapshotNow, 1, &scankey);
+
+    tuple = systable_getnext(scan);
+    if (!HeapTupleIsValid(tuple))
+        elog(ERROR, "could not find tuple for transform %u", transformOid);
+    simple_heap_delete(relation, &tuple->t_self);
+
+    systable_endscan(scan);
+    heap_close(relation, RowExclusiveLock);
+}
+
+
 /*
  * Subroutine for ALTER FUNCTION/AGGREGATE SET SCHEMA/RENAME
  *
diff --git a/src/backend/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c
index b5b8d63..4dd60ea 100644
--- a/src/backend/nodes/copyfuncs.c
+++ b/src/backend/nodes/copyfuncs.c
@@ -3476,6 +3476,20 @@
     return newnode;
 }

+static CreateTransformStmt *
+_copyCreateTransformStmt(const CreateTransformStmt *from)
+{
+    CreateTransformStmt *newnode = makeNode(CreateTransformStmt);
+
+    COPY_SCALAR_FIELD(replace);
+    COPY_NODE_FIELD(type_name);
+    COPY_STRING_FIELD(lang);
+    COPY_NODE_FIELD(fromsql);
+    COPY_NODE_FIELD(tosql);
+
+    return newnode;
+}
+
 static CreateTrigStmt *
 _copyCreateTrigStmt(const CreateTrigStmt *from)
 {
@@ -4377,6 +4391,9 @@
         case T_CreateForeignTableStmt:
             retval = _copyCreateForeignTableStmt(from);
             break;
+        case T_CreateTransformStmt:
+            retval = _copyCreateTransformStmt(from);
+            break;
         case T_CreateTrigStmt:
             retval = _copyCreateTrigStmt(from);
             break;
diff --git a/src/backend/nodes/equalfuncs.c b/src/backend/nodes/equalfuncs.c
index 3f96595..44f4b8f 100644
--- a/src/backend/nodes/equalfuncs.c
+++ b/src/backend/nodes/equalfuncs.c
@@ -1721,6 +1721,18 @@
 }

 static bool
+_equalCreateTransformStmt(const CreateTransformStmt *a, const CreateTransformStmt *b)
+{
+    COMPARE_SCALAR_FIELD(replace);
+    COMPARE_NODE_FIELD(type_name);
+    COMPARE_STRING_FIELD(lang);
+    COMPARE_NODE_FIELD(fromsql);
+    COMPARE_NODE_FIELD(tosql);
+
+    return true;
+}
+
+static bool
 _equalCreateTrigStmt(const CreateTrigStmt *a, const CreateTrigStmt *b)
 {
     COMPARE_STRING_FIELD(trigname);
@@ -2847,6 +2859,9 @@
         case T_CreateForeignTableStmt:
             retval = _equalCreateForeignTableStmt(a, b);
             break;
+        case T_CreateTransformStmt:
+            retval = _equalCreateTransformStmt(a, b);
+            break;
         case T_CreateTrigStmt:
             retval = _equalCreateTrigStmt(a, b);
             break;
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 5094226..afc33e3 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -227,12 +227,12 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
         CreateOpFamilyStmt AlterOpFamilyStmt CreatePLangStmt
         CreateSchemaStmt CreateSeqStmt CreateStmt CreateTableSpaceStmt
         CreateFdwStmt CreateForeignServerStmt CreateForeignTableStmt
-        CreateAssertStmt CreateTrigStmt CreateEventTrigStmt
+        CreateAssertStmt CreateTransformStmt CreateTrigStmt CreateEventTrigStmt
         CreateUserStmt CreateUserMappingStmt CreateRoleStmt
         CreatedbStmt DeclareCursorStmt DefineStmt DeleteStmt DiscardStmt DoStmt
         DropGroupStmt DropOpClassStmt DropOpFamilyStmt DropPLangStmt DropStmt
         DropAssertStmt DropTrigStmt DropRuleStmt DropCastStmt DropRoleStmt
-        DropUserStmt DropdbStmt DropTableSpaceStmt DropFdwStmt
+        DropUserStmt DropdbStmt DropTableSpaceStmt DropFdwStmt DropTransformStmt
         DropForeignServerStmt DropUserMappingStmt ExplainStmt FetchStmt
         GrantStmt GrantRoleStmt IndexStmt InsertStmt ListenStmt LoadStmt
         LockStmt NotifyStmt ExplainableStmt PreparableStmt
@@ -344,6 +344,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
                 opt_enum_val_list enum_val_list table_func_column_list
                 create_generic_options alter_generic_options
                 relation_expr_list dostmt_opt_list
+                transform_element_list

 %type <list>    opt_fdw_options fdw_options
 %type <defelt>    fdw_option
@@ -578,12 +579,12 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);

     SAVEPOINT SCHEMA SCROLL SEARCH SECOND_P SECURITY SELECT SEQUENCE SEQUENCES
     SERIALIZABLE SERVER SESSION SESSION_USER SET SETOF SHARE
-    SHOW SIMILAR SIMPLE SMALLINT SNAPSHOT SOME STABLE STANDALONE_P START
+    SHOW SIMILAR SIMPLE SMALLINT SNAPSHOT SOME SQL_P STABLE STANDALONE_P START
     STATEMENT STATISTICS STDIN STDOUT STORAGE STRICT_P STRIP_P SUBSTRING
     SYMMETRIC SYSID SYSTEM_P

     TABLE TABLES TABLESPACE TEMP TEMPLATE TEMPORARY TEXT_P THEN TIME TIMESTAMP
-    TO TRAILING TRANSACTION TREAT TRIGGER TRIM TRUE_P
+    TO TRAILING TRANSACTION TRANSFORM TREAT TRIGGER TRIM TRUE_P
     TRUNCATE TRUSTED TYPE_P TYPES_P

     UNBOUNDED UNCOMMITTED UNENCRYPTED UNION UNIQUE UNKNOWN UNLISTEN UNLOGGED
@@ -753,6 +754,7 @@ stmt :
             | CreateSeqStmt
             | CreateStmt
             | CreateTableSpaceStmt
+            | CreateTransformStmt
             | CreateTrigStmt
             | CreateEventTrigStmt
             | CreateRoleStmt
@@ -777,6 +779,7 @@ stmt :
             | DropRuleStmt
             | DropStmt
             | DropTableSpaceStmt
+            | DropTransformStmt
             | DropTrigStmt
             | DropRoleStmt
             | DropUserStmt
@@ -3847,6 +3850,16 @@ AlterExtensionContentsStmt:
                     n->objname = list_make1(makeString($6));
                     $$ = (Node *)n;
                 }
+            | ALTER EXTENSION name add_drop TRANSFORM FOR Typename LANGUAGE name
+                {
+                    AlterExtensionContentsStmt *n = makeNode(AlterExtensionContentsStmt);
+                    n->extname = $3;
+                    n->action = $4;
+                    n->objtype = OBJECT_TRANSFORM;
+                    n->objname = list_make1($7);
+                    n->objargs = list_make1($9);
+                    $$ = (Node *)n;
+                }
             | ALTER EXTENSION name add_drop TYPE_P any_name
                 {
                     AlterExtensionContentsStmt *n = makeNode(AlterExtensionContentsStmt);
@@ -5274,6 +5287,15 @@ CommentStmt:
                     n->comment = $6;
                     $$ = (Node *) n;
                 }
+            | COMMENT ON TRANSFORM FOR Typename LANGUAGE name IS comment_text
+                {
+                    CommentStmt *n = makeNode(CommentStmt);
+                    n->objtype = OBJECT_TRANSFORM;
+                    n->objname = list_make1($5);
+                    n->objargs = list_make1($7);
+                    n->comment = $9;
+                    $$ = (Node *) n;
+                }
             | COMMENT ON TRIGGER name ON any_name IS comment_text
                 {
                     CommentStmt *n = makeNode(CommentStmt);
@@ -6744,6 +6766,56 @@ opt_if_exists: IF_P EXISTS                        { $$ = TRUE; }

 /*****************************************************************************
  *
+ *        CREATE TRANSFORM / DROP TRANSFORM
+ *
+ *****************************************************************************/
+
+CreateTransformStmt: CREATE opt_or_replace TRANSFORM FOR Typename LANGUAGE name '(' transform_element_list ')'
+                {
+                    CreateTransformStmt *n = makeNode(CreateTransformStmt);
+                    n->replace = $2;
+                    n->type_name = $5;
+                    n->lang = $7;
+                    n->fromsql = linitial($9);
+                    n->tosql = lsecond($9);
+                    $$ = (Node *)n;
+                }
+        ;
+
+transform_element_list: FROM SQL_P WITH FUNCTION function_with_argtypes ',' TO SQL_P WITH FUNCTION
function_with_argtypes
+                {
+                    $$ = list_make2($5, $11);
+                }
+                | TO SQL_P WITH FUNCTION function_with_argtypes ',' FROM SQL_P WITH FUNCTION function_with_argtypes
+                {
+                    $$ = list_make2($11, $5);
+                }
+                | FROM SQL_P WITH FUNCTION function_with_argtypes
+                {
+                    $$ = list_make2($5, NULL);
+                }
+                | TO SQL_P WITH FUNCTION function_with_argtypes
+                {
+                    $$ = list_make2(NULL, $5);
+                }
+        ;
+
+
+DropTransformStmt: DROP TRANSFORM opt_if_exists FOR Typename LANGUAGE name opt_drop_behavior
+                {
+                    DropStmt *n = makeNode(DropStmt);
+                    n->removeType = OBJECT_TRANSFORM;
+                    n->objects = list_make1(list_make1($5));
+                    n->arguments = list_make1(list_make1($7));
+                    n->behavior = $8;
+                    n->missing_ok = $3;
+                    $$ = (Node *)n;
+                }
+        ;
+
+
+/*****************************************************************************
+ *
  *        QUERY:
  *
  *        REINDEX type <name> [FORCE]
@@ -12851,6 +12923,7 @@ unreserved_keyword:
             | SHOW
             | SIMPLE
             | SNAPSHOT
+            | SQL_P
             | STABLE
             | STANDALONE_P
             | START
@@ -12870,6 +12943,7 @@ unreserved_keyword:
             | TEMPORARY
             | TEXT_P
             | TRANSACTION
+            | TRANSFORM
             | TRIGGER
             | TRUNCATE
             | TRUSTED
diff --git a/src/backend/parser/parse_func.c b/src/backend/parser/parse_func.c
index ae7d195..7c99938 100644
--- a/src/backend/parser/parse_func.c
+++ b/src/backend/parser/parse_func.c
@@ -306,6 +306,11 @@ static Node *ParseComplexProjection(ParseState *pstate, char *funcname,
                      parser_errposition(pstate, location)));
     }

+    if (rettype == INTERNALOID)
+        ereport(ERROR,
+                (errcode(ERRCODE_WRONG_OBJECT_TYPE),
+                 errmsg("cannot call function returning type \"internal\"")));
+
     /*
      * If there are default arguments, we have to include their types in
      * actual_arg_types for the purpose of checking generic type consistency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index c940897..3466b9c 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -214,6 +214,7 @@ static void ProcessUtilitySlow(Node *parsetree,
         case T_CreateTableAsStmt:
         case T_RefreshMatViewStmt:
         case T_CreateTableSpaceStmt:
+        case T_CreateTransformStmt:
         case T_CreateTrigStmt:
         case T_CompositeTypeStmt:
         case T_CreateEnumStmt:
@@ -1290,6 +1291,10 @@ static void ProcessUtilitySlow(Node *parsetree,
                 DefineOpFamily((CreateOpFamilyStmt *) parsetree);
                 break;

+            case T_CreateTransformStmt:
+                CreateTransform((CreateTransformStmt *) parsetree);
+                break;
+
             case T_AlterOpFamilyStmt:
                 AlterOpFamily((AlterOpFamilyStmt *) parsetree);
                 break;
@@ -1946,6 +1951,9 @@ static void ProcessUtilitySlow(Node *parsetree,
                 case OBJECT_OPFAMILY:
                     tag = "DROP OPERATOR FAMILY";
                     break;
+                case OBJECT_TRANSFORM:
+                    tag = "DROP TRANSFORM";
+                    break;
                 default:
                     tag = "???";
             }
@@ -2194,6 +2202,10 @@ static void ProcessUtilitySlow(Node *parsetree,
             }
             break;

+        case T_CreateTransformStmt:
+            tag = "CREATE TRANSFORM";
+            break;
+
         case T_CreateTrigStmt:
             tag = "CREATE TRIGGER";
             break;
@@ -2809,6 +2821,10 @@ static void ProcessUtilitySlow(Node *parsetree,
             lev = LOGSTMT_DDL;
             break;

+        case T_CreateTransformStmt:
+            lev = LOGSTMT_DDL;
+            break;
+
         case T_AlterOpFamilyStmt:
             lev = LOGSTMT_DDL;
             break;
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index a1ed781..f50f374 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -1869,9 +1869,7 @@ static char *generate_function_name(Oid funcid, int nargs,
     StringInfoData buf;
     StringInfoData dq;
     HeapTuple    proctup;
-    HeapTuple    langtup;
     Form_pg_proc proc;
-    Form_pg_language lang;
     Datum        tmp;
     bool        isnull;
     const char *prosrc;
@@ -1894,12 +1892,6 @@ static char *generate_function_name(Oid funcid, int nargs,
                 (errcode(ERRCODE_WRONG_OBJECT_TYPE),
                  errmsg("\"%s\" is an aggregate function", name)));

-    /* Need its pg_language tuple for the language name */
-    langtup = SearchSysCache1(LANGOID, ObjectIdGetDatum(proc->prolang));
-    if (!HeapTupleIsValid(langtup))
-        elog(ERROR, "cache lookup failed for language %u", proc->prolang);
-    lang = (Form_pg_language) GETSTRUCT(langtup);
-
     /*
      * We always qualify the function name, to ensure the right function gets
      * replaced.
@@ -1911,7 +1903,7 @@ static char *generate_function_name(Oid funcid, int nargs,
     appendStringInfoString(&buf, ")\n RETURNS ");
     print_function_rettype(&buf, proctup);
     appendStringInfo(&buf, "\n LANGUAGE %s\n",
-                     quote_identifier(NameStr(lang->lanname)));
+                     quote_identifier(get_language_name(proc->prolang, false)));

     /* Emit some miscellaneous options on one line */
     oldlen = buf.len;
@@ -2031,7 +2023,6 @@ static char *generate_function_name(Oid funcid, int nargs,

     appendStringInfoString(&buf, "\n");

-    ReleaseSysCache(langtup);
     ReleaseSysCache(proctup);

     PG_RETURN_TEXT_P(string_to_text(buf.data));
diff --git a/src/backend/utils/cache/lsyscache.c b/src/backend/utils/cache/lsyscache.c
index 5865962..fd08a7d 100644
--- a/src/backend/utils/cache/lsyscache.c
+++ b/src/backend/utils/cache/lsyscache.c
@@ -23,12 +23,14 @@
 #include "catalog/pg_amproc.h"
 #include "catalog/pg_collation.h"
 #include "catalog/pg_constraint.h"
+#include "catalog/pg_language.h"
 #include "catalog/pg_namespace.h"
 #include "catalog/pg_opclass.h"
 #include "catalog/pg_operator.h"
 #include "catalog/pg_proc.h"
 #include "catalog/pg_range.h"
 #include "catalog/pg_statistic.h"
+#include "catalog/pg_transform.h"
 #include "catalog/pg_type.h"
 #include "miscadmin.h"
 #include "nodes/makefuncs.h"
@@ -1032,6 +1034,30 @@
         return NULL;
 }

+/*                ---------- LANGUAGE CACHE ----------                     */
+
+char *
+get_language_name(Oid langoid, bool missing_ok)
+{
+    HeapTuple    tp;
+
+    tp = SearchSysCache1(LANGOID, ObjectIdGetDatum(langoid));
+    if (HeapTupleIsValid(tp))
+    {
+        Form_pg_language lantup = (Form_pg_language) GETSTRUCT(tp);
+        char       *result;
+
+        result = pstrdup(NameStr(lantup->lanname));
+        ReleaseSysCache(tp);
+        return result;
+    }
+
+    if (!missing_ok)
+        elog(ERROR, "cache lookup failed for language %u",
+             langoid);
+    return NULL;
+}
+
 /*                ---------- OPCLASS CACHE ----------                         */

 /*
@@ -1779,6 +1805,63 @@
 }


+/*                ---------- TRANSFORM CACHE ----------                         */
+
+Oid
+get_transform(Oid typid, Oid langid)
+{
+    HeapTuple    tup;
+
+    tup = SearchSysCache2(TRFTYPELANG, typid, langid);
+    if (HeapTupleIsValid(tup))
+    {
+        Oid            id;
+
+        id = HeapTupleGetOid(tup);
+        ReleaseSysCache(tup);
+        return id;
+    }
+    else
+        return InvalidOid;
+}
+
+Oid
+get_transform_fromsql(Oid typid, Oid langid)
+{
+    HeapTuple    tup;
+
+    tup = SearchSysCache2(TRFTYPELANG, typid, langid);
+    if (HeapTupleIsValid(tup))
+    {
+        Oid            funcid;
+
+        funcid = ((Form_pg_transform) GETSTRUCT(tup))->trffromsql;
+        ReleaseSysCache(tup);
+        return funcid;
+    }
+    else
+        return InvalidOid;
+}
+
+Oid
+get_transform_tosql(Oid typid, Oid langid)
+{
+    HeapTuple    tup;
+
+    tup = SearchSysCache2(TRFTYPELANG, typid, langid);
+    if (HeapTupleIsValid(tup))
+    {
+        Oid            funcid;
+
+        funcid = ((Form_pg_transform) GETSTRUCT(tup))->trftosql;
+        ReleaseSysCache(tup);
+        return funcid;
+    }
+    else
+        return InvalidOid;
+}
+
+
 /*                ---------- TYPE CACHE ----------                         */

 /*
diff --git a/src/backend/utils/cache/syscache.c b/src/backend/utils/cache/syscache.c
index ecb0f96..6e6cef4 100644
--- a/src/backend/utils/cache/syscache.c
+++ b/src/backend/utils/cache/syscache.c
@@ -49,6 +49,7 @@
 #include "catalog/pg_rewrite.h"
 #include "catalog/pg_statistic.h"
 #include "catalog/pg_tablespace.h"
+#include "catalog/pg_transform.h"
 #include "catalog/pg_ts_config.h"
 #include "catalog/pg_ts_config_map.h"
 #include "catalog/pg_ts_dict.h"
@@ -646,6 +647,28 @@ struct cachedesc
         },
         16
     },
+    {TransformRelationId,        /* TRFOID */
+     TransformOidIndexId,
+     1,
+     {
+         ObjectIdAttributeNumber,
+         0,
+         0,
+         0,
+     },
+     16
+    },
+    {TransformRelationId,        /* TRFTYPELANG */
+     TransformTypeLangIndexId,
+     2,
+     {
+         Anum_pg_transform_trftype,
+         Anum_pg_transform_trflang,
+         0,
+         0,
+     },
+     16
+    },
     {TSConfigMapRelationId,        /* TSCONFIGMAP */
         TSConfigMapIndexId,
         3,
diff --git a/src/bin/pg_dump/common.c b/src/bin/pg_dump/common.c
index 58322dc..212d4d7 100644
--- a/src/bin/pg_dump/common.c
+++ b/src/bin/pg_dump/common.c
@@ -89,6 +89,7 @@ static void findParentsByOid(TableInfo *self,
     int            numRules;
     int            numProcLangs;
     int            numCasts;
+    int            numTransforms;
     int            numOpclasses;
     int            numOpfamilies;
     int            numConversions;
@@ -199,6 +200,10 @@ static void findParentsByOid(TableInfo *self,
     getCasts(fout, &numCasts);

     if (g_verbose)
+        write_msg(NULL, "reading transforms\n");
+    getTransforms(fout, &numTransforms);
+
+    if (g_verbose)
         write_msg(NULL, "reading table inheritance information\n");
     inhinfo = getInherits(fout, &numInherits);

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index ec956ad..c4da9e5 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -182,6 +182,7 @@ static int findSecLabels(Archive *fout, Oid classoid, Oid objoid,
 static void dumpProcLang(Archive *fout, ProcLangInfo *plang);
 static void dumpFunc(Archive *fout, FuncInfo *finfo);
 static void dumpCast(Archive *fout, CastInfo *cast);
+static void dumpTransform(Archive *fout, TransformInfo *transform);
 static void dumpOpr(Archive *fout, OprInfo *oprinfo);
 static void dumpOpclass(Archive *fout, OpclassInfo *opcinfo);
 static void dumpOpfamily(Archive *fout, OpfamilyInfo *opfinfo);
@@ -6097,6 +6098,110 @@ static void binary_upgrade_extension_member(PQExpBuffer upgrade_buffer,
     return castinfo;
 }

+static char *
+get_language_name(Archive *fout, Oid langid)
+{
+    PQExpBuffer query;
+    PGresult   *res;
+    char       *lanname;
+
+    query = createPQExpBuffer();
+    appendPQExpBuffer(query, "SELECT lanname FROM pg_language WHERE oid = %u", langid);
+    res = ExecuteSqlQueryForSingleRow(fout, query->data);
+    lanname = pg_strdup(fmtId(PQgetvalue(res, 0, 0)));
+    destroyPQExpBuffer(query);
+    PQclear(res);
+
+    return lanname;
+}
+
+/*
+ * getTransforms
+ *      get basic information about every transform in the system
+ *
+ * numTransforms is set to the number of transforms read in
+ */
+TransformInfo *
+getTransforms(Archive *fout, int *numTransforms)
+{
+    PGresult   *res;
+    int            ntups;
+    int            i;
+    PQExpBuffer query = createPQExpBuffer();
+    TransformInfo   *transforminfo;
+    int            i_tableoid;
+    int            i_oid;
+    int            i_trftype;
+    int            i_trflang;
+    int            i_trffromsql;
+    int            i_trftosql;
+
+    /* Transforms didn't exist pre-9.4 */
+    if (fout->remoteVersion < 90400)
+    {
+        *numTransforms = 0;
+        return NULL;
+    }
+
+    /* Make sure we are in proper schema */
+    selectSourceSchema(fout, "pg_catalog");
+
+    appendPQExpBuffer(query, "SELECT tableoid, oid, "
+                      "trftype, trflang, trffromsql::oid, trftosql::oid "
+                      "FROM pg_transform "
+                      "ORDER BY 3,4");
+
+    res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK);
+
+    ntups = PQntuples(res);
+
+    *numTransforms = ntups;
+
+    transforminfo = (TransformInfo *) pg_malloc(ntups * sizeof(TransformInfo));
+
+    i_tableoid = PQfnumber(res, "tableoid");
+    i_oid = PQfnumber(res, "oid");
+    i_trftype = PQfnumber(res, "trftype");
+    i_trflang = PQfnumber(res, "trflang");
+    i_trffromsql = PQfnumber(res, "trffromsql");
+    i_trftosql = PQfnumber(res, "trftosql");
+
+    for (i = 0; i < ntups; i++)
+    {
+        PQExpBufferData namebuf;
+        TypeInfo   *typeInfo;
+        char       *lanname;
+
+        transforminfo[i].dobj.objType = DO_TRANSFORM;
+        transforminfo[i].dobj.catId.tableoid = atooid(PQgetvalue(res, i, i_tableoid));
+        transforminfo[i].dobj.catId.oid = atooid(PQgetvalue(res, i, i_oid));
+        AssignDumpId(&transforminfo[i].dobj);
+        transforminfo[i].trftype = atooid(PQgetvalue(res, i, i_trftype));
+        transforminfo[i].trflang = atooid(PQgetvalue(res, i, i_trflang));
+        transforminfo[i].trffromsql = atooid(PQgetvalue(res, i, i_trffromsql));
+        transforminfo[i].trftosql = atooid(PQgetvalue(res, i, i_trftosql));
+
+        /*
+         * Try to name transform as concatenation of type and language name.
+         * This is only used for purposes of sorting.  If we fail to find
+         * either, the name will be an empty string.
+         */
+        initPQExpBuffer(&namebuf);
+        typeInfo = findTypeByOid(transforminfo[i].trftype);
+        lanname = get_language_name(fout, transforminfo[i].trflang);
+        if (typeInfo && lanname)
+            appendPQExpBuffer(&namebuf, "%s %s",
+                              typeInfo->dobj.name, lanname);
+        transforminfo[i].dobj.name = namebuf.data;
+    }
+
+    PQclear(res);
+
+    destroyPQExpBuffer(query);
+
+    return transforminfo;
+}
+
 /*
  * getTableAttrs -
  *      for each interesting table, read info about its attributes
@@ -7707,6 +7812,9 @@ static void binary_upgrade_extension_member(PQExpBuffer upgrade_buffer,
         case DO_CAST:
             dumpCast(fout, (CastInfo *) dobj);
             break;
+        case DO_TRANSFORM:
+            dumpTransform(fout, (TransformInfo *) dobj);
+            break;
         case DO_TABLE_DATA:
             if (((TableDataInfo *) dobj)->tdtable->relkind == RELKIND_SEQUENCE)
                 dumpSequenceData(fout, (TableDataInfo *) dobj);
@@ -10098,6 +10206,127 @@ static void binary_upgrade_extension_member(PQExpBuffer upgrade_buffer,
 }

 /*
+ * Dump a transform
+ */
+static void
+dumpTransform(Archive *fout, TransformInfo *transform)
+{
+    PQExpBuffer defqry;
+    PQExpBuffer delqry;
+    PQExpBuffer labelq;
+    FuncInfo   *fromsqlFuncInfo = NULL;
+    FuncInfo   *tosqlFuncInfo = NULL;
+    char       *lanname;
+
+    /* Skip if not to be dumped */
+    if (!transform->dobj.dump || dataOnly)
+        return;
+
+    /* Cannot dump if we don't have the transform functions' info */
+    if (OidIsValid(transform->trffromsql))
+    {
+        fromsqlFuncInfo = findFuncByOid(transform->trffromsql);
+        if (fromsqlFuncInfo == NULL)
+            return;
+    }
+    if (OidIsValid(transform->trftosql))
+    {
+        tosqlFuncInfo = findFuncByOid(transform->trftosql);
+        if (tosqlFuncInfo == NULL)
+            return;
+    }
+
+    /* Make sure we are in proper schema (needed for getFormattedTypeName) */
+    selectSourceSchema(fout, "pg_catalog");
+
+    defqry = createPQExpBuffer();
+    delqry = createPQExpBuffer();
+    labelq = createPQExpBuffer();
+
+    lanname = get_language_name(fout, transform->trflang);
+
+    appendPQExpBuffer(delqry, "DROP TRANSFORM FOR %s LANGUAGE %s;\n",
+                      getFormattedTypeName(fout, transform->trftype, zeroAsNone),
+                      lanname);
+
+    appendPQExpBuffer(defqry, "CREATE TRANSFORM FOR %s LANGUAGE %s (",
+                      getFormattedTypeName(fout, transform->trftype, zeroAsNone),
+                      lanname);
+
+    if (!transform->trffromsql && !transform->trftosql)
+        write_msg(NULL, "WARNING: bogus transform definition, at least one of trffromsql and trftosql should be
nonzero\n");
+
+    if (transform->trffromsql)
+    {
+        if (fromsqlFuncInfo)
+        {
+            char       *fsig = format_function_signature(fout, fromsqlFuncInfo, true);
+
+            /*
+             * Always qualify the function name, in case it is not in
+             * pg_catalog schema (format_function_signature won't qualify
+             * it).
+             */
+            appendPQExpBuffer(defqry, "FROM SQL WITH FUNCTION %s.%s",
+                              fmtId(fromsqlFuncInfo->dobj.namespace->dobj.name), fsig);
+            free(fsig);
+        }
+        else
+            write_msg(NULL, "WARNING: bogus value in pg_transform.trffromsql field\n");
+    }
+
+    if (transform->trftosql)
+    {
+        if (transform->trffromsql)
+            appendPQExpBuffer(defqry, ", ");
+
+        if (tosqlFuncInfo)
+        {
+            char       *fsig = format_function_signature(fout, tosqlFuncInfo, true);
+
+            /*
+             * Always qualify the function name, in case it is not in
+             * pg_catalog schema (format_function_signature won't qualify
+             * it).
+             */
+            appendPQExpBuffer(defqry, "TO SQL WITH FUNCTION %s.%s",
+                              fmtId(tosqlFuncInfo->dobj.namespace->dobj.name), fsig);
+            free(fsig);
+        }
+        else
+            write_msg(NULL, "WARNING: bogus value in pg_transform.trftosql field\n");
+    }
+
+    appendPQExpBuffer(defqry, ");\n");
+
+    appendPQExpBuffer(labelq, "TRANSFORM FOR %s LANGUAGE %s",
+                      getFormattedTypeName(fout, transform->trftype, zeroAsNone),
+                      lanname);
+
+    if (binary_upgrade)
+        binary_upgrade_extension_member(defqry, &transform->dobj, labelq->data);
+
+    ArchiveEntry(fout, transform->dobj.catId, transform->dobj.dumpId,
+                 labelq->data,
+                 "pg_catalog", NULL, "",
+                 false, "TRANSFORM", SECTION_PRE_DATA,
+                 defqry->data, delqry->data, NULL,
+                 transform->dobj.dependencies, transform->dobj.nDeps,
+                 NULL, NULL);
+
+    /* Dump Transform Comments */
+    dumpComment(fout, labelq->data,
+                NULL, "",
+                transform->dobj.catId, 0, transform->dobj.dumpId);
+
+    free(lanname);
+    destroyPQExpBuffer(defqry);
+    destroyPQExpBuffer(delqry);
+    destroyPQExpBuffer(labelq);
+}
+
+
+/*
  * dumpOpr
  *      write out a single operator definition
  */
@@ -14895,6 +15124,7 @@ static void binary_upgrade_extension_member(PQExpBuffer upgrade_buffer,
             case DO_TSCONFIG:
             case DO_FDW:
             case DO_FOREIGN_SERVER:
+            case DO_TRANSFORM:
             case DO_BLOB:
                 /* Pre-data objects: must come before the pre-data boundary */
                 addObjectDependency(preDataBound, dobj->dumpId);
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index 5582538..5add50e 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -106,6 +106,7 @@ typedef enum
     DO_FDW,
     DO_FOREIGN_SERVER,
     DO_DEFAULT_ACL,
+    DO_TRANSFORM,
     DO_BLOB,
     DO_BLOB_DATA,
     DO_PRE_DATA_BOUNDARY,
@@ -406,6 +407,15 @@ typedef struct _castInfo
     char        castmethod;
 } CastInfo;

+typedef struct _transformInfo
+{
+    DumpableObject dobj;
+    Oid            trftype;
+    Oid            trflang;
+    Oid            trffromsql;
+    Oid            trftosql;
+} TransformInfo;
+
 /* InhInfo isn't a DumpableObject, just temporary state */
 typedef struct _inhInfo
 {
@@ -558,6 +568,7 @@ extern RuleInfo *getRules(Archive *fout, int *numRules);
 extern void getTriggers(Archive *fout, TableInfo tblinfo[], int numTables);
 extern ProcLangInfo *getProcLangs(Archive *fout, int *numProcLangs);
 extern CastInfo *getCasts(Archive *fout, int *numCasts);
+extern TransformInfo *getTransforms(Archive *fout, int *numTransforms);
 extern void getTableAttrs(Archive *fout, TableInfo *tbinfo, int numTables);
 extern bool shouldPrintColumn(TableInfo *tbinfo, int colno);
 extern TSParserInfo *getTSParsers(Archive *fout, int *numTSParsers);
diff --git a/src/bin/pg_dump/pg_dump_sort.c b/src/bin/pg_dump/pg_dump_sort.c
index 141e713..9d0a6d1 100644
--- a/src/bin/pg_dump/pg_dump_sort.c
+++ b/src/bin/pg_dump/pg_dump_sort.c
@@ -26,8 +26,8 @@
  * by OID.    (This is a relatively crude hack to provide semi-reasonable
  * behavior for old databases without full dependency info.)  Note: collations,
  * extensions, text search, foreign-data, materialized view, event trigger,
- * and default ACL objects can't really happen here, so the rather bogus
- * priorities for them don't matter.
+ * transforms, and default ACL objects can't really happen here, so the rather
+ * bogus priorities for them don't matter.
  *
  * NOTE: object-type priorities must match the section assignments made in
  * pg_dump.c; that is, PRE_DATA objects must sort before DO_PRE_DATA_BOUNDARY,
@@ -65,6 +65,7 @@
     4,                            /* DO_FDW */
     4,                            /* DO_FOREIGN_SERVER */
     19,                            /* DO_DEFAULT_ACL */
+    4,                            /* DO_TRANSFORM */
     9,                            /* DO_BLOB */
     12,                            /* DO_BLOB_DATA */
     10,                            /* DO_PRE_DATA_BOUNDARY */
@@ -113,6 +114,7 @@
     16,                            /* DO_FDW */
     17,                            /* DO_FOREIGN_SERVER */
     31,                            /* DO_DEFAULT_ACL */
+    3,                            /* DO_TRANSFORM */
     21,                            /* DO_BLOB */
     24,                            /* DO_BLOB_DATA */
     22,                            /* DO_PRE_DATA_BOUNDARY */
@@ -1287,6 +1289,13 @@ static void describeDumpableObject(DumpableObject *obj,
                      ((CastInfo *) obj)->casttarget,
                      obj->dumpId, obj->catId.oid);
             return;
+        case DO_TRANSFORM:
+            snprintf(buf, bufsize,
+                     "TRANSFORM %u lang %u  (ID %d OID %u)",
+                     ((TransformInfo *) obj)->trftype,
+                     ((TransformInfo *) obj)->trflang,
+                     obj->dumpId, obj->catId.oid);
+            return;
         case DO_TABLE_DATA:
             snprintf(buf, bufsize,
                      "TABLE DATA %s  (ID %d OID %u)",
diff --git a/src/include/catalog/catversion.h b/src/include/catalog/catversion.h
index d46fe9e..283d345 100644
--- a/src/include/catalog/catversion.h
+++ b/src/include/catalog/catversion.h
@@ -53,6 +53,6 @@
  */

 /*                            yyyymmddN */
-#define CATALOG_VERSION_NO    201306121
+#define CATALOG_VERSION_NO    201306132

 #endif
diff --git a/src/include/catalog/dependency.h b/src/include/catalog/dependency.h
index 3aefbb5e..404fda4 100644
--- a/src/include/catalog/dependency.h
+++ b/src/include/catalog/dependency.h
@@ -147,6 +147,7 @@ typedef enum ObjectClass
     OCLASS_DEFACL,                /* pg_default_acl */
     OCLASS_EXTENSION,            /* pg_extension */
     OCLASS_EVENT_TRIGGER,        /* pg_event_trigger */
+    OCLASS_TRANSFORM,            /* pg_transform */
     MAX_OCLASS                    /* MUST BE LAST */
 } ObjectClass;

diff --git a/src/include/catalog/indexing.h b/src/include/catalog/indexing.h
index 19268fb..86c7dba 100644
--- a/src/include/catalog/indexing.h
+++ b/src/include/catalog/indexing.h
@@ -226,6 +226,11 @@ DECLARE_UNIQUE_INDEX(pg_tablespace_oid_index, 2697, on pg_tablespace using btree
 DECLARE_UNIQUE_INDEX(pg_tablespace_spcname_index, 2698, on pg_tablespace using btree(spcname name_ops));
 #define TablespaceNameIndexId  2698

+DECLARE_UNIQUE_INDEX(pg_transform_oid_index, 3780, on pg_transform using btree(oid oid_ops));
+#define TransformOidIndexId    3780
+DECLARE_UNIQUE_INDEX(pg_transform_type_lang_index, 3781, on pg_transform using btree(trftype oid_ops, trflang
oid_ops));
+#define TransformTypeLangIndexId  3781
+
 /* This following index is not used for a cache and is not unique */
 DECLARE_INDEX(pg_trigger_tgconstraint_index, 2699, on pg_trigger using btree(tgconstraint oid_ops));
 #define TriggerConstraintIndexId  2699
diff --git a/src/include/catalog/pg_transform.h b/src/include/catalog/pg_transform.h
new file mode 100644
index 0000000..80b191e
--- /dev/null
+++ b/src/include/catalog/pg_transform.h
@@ -0,0 +1,47 @@
+/*-------------------------------------------------------------------------
+ *
+ * pg_transform.h
+ *
+ * Copyright (c) 2012, PostgreSQL Global Development Group
+ *
+ * src/include/catalog/pg_transform.h
+ *
+ * NOTES
+ *      the genbki.pl script reads this file and generates .bki
+ *      information from the DATA() statements.
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef PG_TRANSFORM_H
+#define PG_TRANSFORM_H
+
+#include "catalog/genbki.h"
+
+/* ----------------
+ *        pg_transform definition.  cpp turns this into
+ *        typedef struct FormData_pg_transform
+ * ----------------
+ */
+#define TransformRelationId    3779
+
+CATALOG(pg_transform,3779)
+{
+    Oid            trftype;
+    Oid            trflang;
+    regproc        trffromsql;
+    regproc        trftosql;
+} FormData_pg_transform;
+
+typedef FormData_pg_transform *Form_pg_transform;
+
+/* ----------------
+ *        compiler constants for pg_transform
+ * ----------------
+ */
+#define Natts_pg_transform            4
+#define Anum_pg_transform_trftype    1
+#define Anum_pg_transform_trflang    2
+#define Anum_pg_transform_trffromsql    3
+#define Anum_pg_transform_trftosql    4
+
+#endif   /* PG_TRANSFORM_H */
diff --git a/src/include/commands/defrem.h b/src/include/commands/defrem.h
index fa9f41f..deac301 100644
--- a/src/include/commands/defrem.h
+++ b/src/include/commands/defrem.h
@@ -49,10 +49,13 @@ extern void SetFunctionArgType(Oid funcOid, int argIndex, Oid newArgType);
 extern Oid    AlterFunction(AlterFunctionStmt *stmt);
 extern Oid    CreateCast(CreateCastStmt *stmt);
 extern void DropCastById(Oid castOid);
+extern Oid CreateTransform(CreateTransformStmt *stmt);
+extern void DropTransformById(Oid transformOid);
 extern void IsThereFunctionInNamespace(const char *proname, int pronargs,
                            oidvector *proargtypes, Oid nspOid);
 extern void ExecuteDoStmt(DoStmt *stmt);
 extern Oid    get_cast_oid(Oid sourcetypeid, Oid targettypeid, bool missing_ok);
+extern Oid get_transform_oid(Oid typeid, Oid langid, bool missing_ok);

 /* commands/operatorcmds.c */
 extern Oid    DefineOperator(List *names, List *parameters);
diff --git a/src/include/nodes/nodes.h b/src/include/nodes/nodes.h
index 0d5c007..1c082e9 100644
--- a/src/include/nodes/nodes.h
+++ b/src/include/nodes/nodes.h
@@ -362,6 +362,7 @@ typedef enum NodeTag
     T_CreateEventTrigStmt,
     T_AlterEventTrigStmt,
     T_RefreshMatViewStmt,
+    T_CreateTransformStmt,

     /*
      * TAGS FOR PARSE TREE NODES (parsenodes.h)
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 6723647..f028068 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -1145,6 +1145,7 @@ typedef enum ObjectType
     OBJECT_SEQUENCE,
     OBJECT_TABLE,
     OBJECT_TABLESPACE,
+    OBJECT_TRANSFORM,
     OBJECT_TRIGGER,
     OBJECT_TSCONFIGURATION,
     OBJECT_TSDICTIONARY,
@@ -2569,6 +2570,20 @@ typedef struct CreateCastStmt
 } CreateCastStmt;

 /* ----------------------
+ *    CREATE TRANSFORM Statement
+ * ----------------------
+ */
+typedef struct CreateTransformStmt
+{
+    NodeTag        type;
+    bool        replace;
+    TypeName   *type_name;
+    char       *lang;
+    FuncWithArgs *fromsql;
+    FuncWithArgs *tosql;
+} CreateTransformStmt;
+
+/* ----------------------
  *        PREPARE Statement
  * ----------------------
  */
diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h
index 68a13b7..88e9ece 100644
--- a/src/include/parser/kwlist.h
+++ b/src/include/parser/kwlist.h
@@ -345,6 +345,7 @@ PG_KEYWORD("simple", SIMPLE, UNRESERVED_KEYWORD)
 PG_KEYWORD("smallint", SMALLINT, COL_NAME_KEYWORD)
 PG_KEYWORD("snapshot", SNAPSHOT, UNRESERVED_KEYWORD)
 PG_KEYWORD("some", SOME, RESERVED_KEYWORD)
+PG_KEYWORD("sql", SQL_P, UNRESERVED_KEYWORD)
 PG_KEYWORD("stable", STABLE, UNRESERVED_KEYWORD)
 PG_KEYWORD("standalone", STANDALONE_P, UNRESERVED_KEYWORD)
 PG_KEYWORD("start", START, UNRESERVED_KEYWORD)
@@ -372,6 +373,7 @@ PG_KEYWORD("timestamp", TIMESTAMP, COL_NAME_KEYWORD)
 PG_KEYWORD("to", TO, RESERVED_KEYWORD)
 PG_KEYWORD("trailing", TRAILING, RESERVED_KEYWORD)
 PG_KEYWORD("transaction", TRANSACTION, UNRESERVED_KEYWORD)
+PG_KEYWORD("transform", TRANSFORM, UNRESERVED_KEYWORD)
 PG_KEYWORD("treat", TREAT, COL_NAME_KEYWORD)
 PG_KEYWORD("trigger", TRIGGER, UNRESERVED_KEYWORD)
 PG_KEYWORD("trim", TRIM, COL_NAME_KEYWORD)
diff --git a/src/include/utils/lsyscache.h b/src/include/utils/lsyscache.h
index 49f459a..9ff411f 100644
--- a/src/include/utils/lsyscache.h
+++ b/src/include/utils/lsyscache.h
@@ -72,6 +72,7 @@ extern void get_atttypetypmodcoll(Oid relid, AttrNumber attnum,
                       Oid *typid, int32 *typmod, Oid *collid);
 extern char *get_collation_name(Oid colloid);
 extern char *get_constraint_name(Oid conoid);
+extern char *get_language_name(Oid langoid, bool missing_ok);
 extern Oid    get_opclass_family(Oid opclass);
 extern Oid    get_opclass_input_type(Oid opclass);
 extern RegProcedure get_opcode(Oid opno);
@@ -102,6 +103,9 @@ extern Oid    get_rel_namespace(Oid relid);
 extern Oid    get_rel_type_id(Oid relid);
 extern char get_rel_relkind(Oid relid);
 extern Oid    get_rel_tablespace(Oid relid);
+extern Oid get_transform(Oid typid, Oid langid);
+extern Oid get_transform_fromsql(Oid typid, Oid langid);
+extern Oid get_transform_tosql(Oid typid, Oid langid);
 extern bool get_typisdefined(Oid typid);
 extern int16 get_typlen(Oid typid);
 extern bool get_typbyval(Oid typid);
diff --git a/src/include/utils/syscache.h b/src/include/utils/syscache.h
index d1d8abe..ee482f5 100644
--- a/src/include/utils/syscache.h
+++ b/src/include/utils/syscache.h
@@ -80,6 +80,8 @@ enum SysCacheIdentifier
     RULERELNAME,
     STATRELATTINH,
     TABLESPACEOID,
+    TRFOID,
+    TRFTYPELANG,
     TSCONFIGMAP,
     TSCONFIGNAMENSP,
     TSCONFIGOID,
diff --git a/src/interfaces/ecpg/preproc/ecpg.tokens b/src/interfaces/ecpg/preproc/ecpg.tokens
index b55138a..68ba925 100644
--- a/src/interfaces/ecpg/preproc/ecpg.tokens
+++ b/src/interfaces/ecpg/preproc/ecpg.tokens
@@ -12,7 +12,7 @@
                 SQL_LONG SQL_NULLABLE SQL_OCTET_LENGTH
                 SQL_OPEN SQL_OUTPUT SQL_REFERENCE
                 SQL_RETURNED_LENGTH SQL_RETURNED_OCTET_LENGTH SQL_SCALE
-                SQL_SECTION SQL_SHORT SQL_SIGNED SQL_SQL SQL_SQLERROR
+                SQL_SECTION SQL_SHORT SQL_SIGNED SQL_SQLERROR
                 SQL_SQLPRINT SQL_SQLWARNING SQL_START SQL_STOP
                 SQL_STRUCT SQL_UNSIGNED SQL_VAR SQL_WHENEVER

diff --git a/src/interfaces/ecpg/preproc/ecpg.trailer b/src/interfaces/ecpg/preproc/ecpg.trailer
index 8258ce2..a6f3af5 100644
--- a/src/interfaces/ecpg/preproc/ecpg.trailer
+++ b/src/interfaces/ecpg/preproc/ecpg.trailer
@@ -1000,7 +1000,7 @@ ecpg_using:    USING using_list    { $$ = EMPTY; }
         | using_descriptor        { $$ = $1; }
         ;

-using_descriptor: USING SQL_SQL SQL_DESCRIPTOR quoted_ident_stringvar
+using_descriptor: USING SQL_P SQL_DESCRIPTOR quoted_ident_stringvar
         {
             add_variable_to_head(&argsinsert, descriptor_variable($4,0), &no_indicator);
             $$ = EMPTY;
@@ -1012,7 +1012,7 @@ using_descriptor: USING SQL_SQL SQL_DESCRIPTOR quoted_ident_stringvar
         }
         ;

-into_descriptor: INTO SQL_SQL SQL_DESCRIPTOR quoted_ident_stringvar
+into_descriptor: INTO SQL_P SQL_DESCRIPTOR quoted_ident_stringvar
         {
             add_variable_to_head(&argsresult, descriptor_variable($4,1), &no_indicator);
             $$ = EMPTY;
@@ -1489,7 +1489,6 @@ ECPGKeywords_vanames:  SQL_BREAK        { $$ = mm_strdup("break"); }
         | SQL_RETURNED_OCTET_LENGTH    { $$ = mm_strdup("returned_octet_length"); }
         | SQL_SCALE                    { $$ = mm_strdup("scale"); }
         | SQL_SECTION                { $$ = mm_strdup("section"); }
-        | SQL_SQL                { $$ = mm_strdup("sql"); }
         | SQL_SQLERROR                { $$ = mm_strdup("sqlerror"); }
         | SQL_SQLPRINT                { $$ = mm_strdup("sqlprint"); }
         | SQL_SQLWARNING            { $$ = mm_strdup("sqlwarning"); }
diff --git a/src/interfaces/ecpg/preproc/ecpg_keywords.c b/src/interfaces/ecpg/preproc/ecpg_keywords.c
index fb54d7b..6c819fd 100644
--- a/src/interfaces/ecpg/preproc/ecpg_keywords.c
+++ b/src/interfaces/ecpg/preproc/ecpg_keywords.c
@@ -67,8 +67,6 @@
     {"section", SQL_SECTION, 0},
     {"short", SQL_SHORT, 0},
     {"signed", SQL_SIGNED, 0},
-    {"sql", SQL_SQL, 0},        /* strange thing, used for into sql descriptor
-                                 * MYDESC; */
     {"sqlerror", SQL_SQLERROR, 0},
     {"sqlprint", SQL_SQLPRINT, 0},
     {"sqlwarning", SQL_SQLWARNING, 0},
diff --git a/src/pl/plperl/GNUmakefile b/src/pl/plperl/GNUmakefile
index e0e31ec..49a4ed8 100644
--- a/src/pl/plperl/GNUmakefile
+++ b/src/pl/plperl/GNUmakefile
@@ -79,15 +79,17 @@ Util.c: Util.xs plperl_helpers.h
 install: all install-lib install-data

 installdirs: installdirs-lib
-    $(MKDIR_P) '$(DESTDIR)$(datadir)/extension'
+    $(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) $(srcdir)/plperl.h $(srcdir)/ppport.h '$(DESTDIR)$(includedir_server)'

 uninstall-data:
     rm -f $(addprefix '$(DESTDIR)$(datadir)/extension'/, $(notdir $(DATA)))
+    rm -f $(addprefix '$(DESTDIR)$(includedir_server)'/, plperl.h ppport.h)

 .PHONY: install-data uninstall-data

diff --git a/src/pl/plperl/plperl.c b/src/pl/plperl/plperl.c
index de8cb0e..851a41f 100644
--- a/src/pl/plperl/plperl.c
+++ b/src/pl/plperl/plperl.c
@@ -110,6 +110,7 @@
     SV           *reference;        /* CODE reference for Perl sub */
     plperl_interp_desc *interp; /* interpreter it's created in */
     bool        fn_readonly;    /* is function readonly (not volatile)? */
+    Oid            lang_oid;
     bool        lanpltrusted;    /* is it plperl, rather than plperlu? */
     bool        fn_retistuple;    /* true, if function returns tuple */
     bool        fn_retisset;    /* true, if function returns set */
@@ -210,6 +211,7 @@
     bool       *nulls;
     int           *nelems;
     FmgrInfo    proc;
+    FmgrInfo    transform_proc;
 } plperl_array_info;

 /**********************************************************************
@@ -1260,6 +1262,7 @@ static SV  *plperl_call_perl_func(plperl_proc_desc *desc,
                    bool *isnull)
 {
     FmgrInfo    tmp;
+    Oid            funcid;

     /* we might recurse */
     check_stack_depth();
@@ -1283,6 +1286,8 @@ static SV  *plperl_call_perl_func(plperl_proc_desc *desc,
         /* must call typinput in case it wants to reject NULL */
         return InputFunctionCall(finfo, NULL, typioparam, typmod);
     }
+    else if ((funcid = get_transform_tosql(typid, current_call_data->prodesc->lang_oid)))
+        return OidFunctionCall1(funcid, PointerGetDatum(sv));
     else if (SvROK(sv))
     {
         /* handle references */
@@ -1395,6 +1400,7 @@ static SV  *plperl_call_perl_func(plperl_proc_desc *desc,
                 typdelim;
     Oid            typioparam;
     Oid            typoutputfunc;
+    Oid            transform_funcid;
     int            i,
                 nitems,
                *dims;
@@ -1402,14 +1408,17 @@ static SV  *plperl_call_perl_func(plperl_proc_desc *desc,
     SV           *av;
     HV           *hv;

-    info = palloc(sizeof(plperl_array_info));
+    info = palloc0(sizeof(plperl_array_info));

     /* get element type information, including output conversion function */
     get_type_io_data(elementtype, IOFunc_output,
                      &typlen, &typbyval, &typalign,
                      &typdelim, &typioparam, &typoutputfunc);

-    perm_fmgr_info(typoutputfunc, &info->proc);
+    if ((transform_funcid = get_transform_fromsql(elementtype, current_call_data->prodesc->lang_oid)))
+        perm_fmgr_info(transform_funcid, &info->transform_proc);
+    else
+        perm_fmgr_info(typoutputfunc, &info->proc);

     info->elem_is_rowtype = type_is_rowtype(elementtype);

@@ -1490,8 +1499,10 @@ static SV  *plperl_call_perl_func(plperl_proc_desc *desc,
         {
             Datum        itemvalue = info->elements[i];

-            /* Handle composite type elements */
-            if (info->elem_is_rowtype)
+            if (info->transform_proc.fn_oid)
+                av_push(result, (SV *) DatumGetPointer(FunctionCall1(&info->transform_proc, itemvalue)));
+            else if (info->elem_is_rowtype)
+                /* Handle composite type elements */
                 av_push(result, plperl_hash_from_datum(itemvalue));
             else
             {
@@ -1778,6 +1789,7 @@ static SV  *plperl_call_perl_func(plperl_proc_desc *desc,
     desc.proname = "inline_code_block";
     desc.fn_readonly = false;

+    desc.lang_oid = codeblock->langOid;
     desc.lanpltrusted = codeblock->langIsTrusted;

     desc.fn_retistuple = false;
@@ -2035,6 +2047,8 @@ static SV  *plperl_call_perl_func(plperl_proc_desc *desc,
     SV           *retval;
     int            i;
     int            count;
+    Oid           *argtypes = NULL;
+    int            nargs = 0;

     ENTER;
     SAVETMPS;
@@ -2042,6 +2056,9 @@ static SV  *plperl_call_perl_func(plperl_proc_desc *desc,
     PUSHMARK(SP);
     EXTEND(sp, desc->nargs);

+    if (fcinfo->flinfo->fn_oid)
+        get_func_signature(fcinfo->flinfo->fn_oid, &argtypes, &nargs);
+
     for (i = 0; i < desc->nargs; i++)
     {
         if (fcinfo->argnull[i])
@@ -2055,9 +2072,12 @@ static SV  *plperl_call_perl_func(plperl_proc_desc *desc,
         else
         {
             SV           *sv;
+            Oid            funcid;

             if (OidIsValid(desc->arg_arraytype[i]))
                 sv = plperl_ref_from_pg_array(fcinfo->arg[i], desc->arg_arraytype[i]);
+            else if ((funcid = get_transform_fromsql(argtypes[i], current_call_data->prodesc->lang_oid)))
+                sv = (SV *) DatumGetPointer(OidFunctionCall1(funcid, fcinfo->arg[i]));
             else
             {
                 char       *tmp;
@@ -2536,6 +2556,7 @@ static SV  *plperl_call_perl_func(plperl_proc_desc *desc,
                  procStruct->prolang);
         }
         langStruct = (Form_pg_language) GETSTRUCT(langTup);
+        prodesc->lang_oid = HeapTupleGetOid(langTup);
         prodesc->lanpltrusted = langStruct->lanpltrusted;
         ReleaseSysCache(langTup);

@@ -2768,9 +2789,12 @@ static SV  *plperl_call_perl_func(plperl_proc_desc *desc,
         else
         {
             SV           *sv;
+            Oid            funcid;

             if (OidIsValid(get_base_element_type(tupdesc->attrs[i]->atttypid)))
                 sv = plperl_ref_from_pg_array(attr, tupdesc->attrs[i]->atttypid);
+            else if ((funcid = get_transform_fromsql(tupdesc->attrs[i]->atttypid,
current_call_data->prodesc->lang_oid)))
+                sv = (SV *) DatumGetPointer(OidFunctionCall1(funcid, attr));
             else
             {
                 char       *outputstr;
diff --git a/src/pl/plperl/plperl_helpers.h b/src/pl/plperl/plperl_helpers.h
index 3e8aa7c..53ff66a 100644
--- a/src/pl/plperl/plperl_helpers.h
+++ b/src/pl/plperl/plperl_helpers.h
@@ -1,6 +1,8 @@
 #ifndef PL_PERL_HELPERS_H
 #define PL_PERL_HELPERS_H

+#include "mb/pg_wchar.h"
+
 /*
  * convert from utf8 to database encoding
  *
diff --git a/src/pl/plpython/Makefile b/src/pl/plpython/Makefile
index 3fe8e4a..d595c5c 100644
--- a/src/pl/plpython/Makefile
+++ b/src/pl/plpython/Makefile
@@ -115,54 +115,22 @@ all: all-lib
 install: all install-lib install-data

 installdirs: installdirs-lib
-    $(MKDIR_P) '$(DESTDIR)$(datadir)/extension'
+    $(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) $(srcdir)/plpython.h $(srcdir)/plpy_util.h '$(DESTDIR)$(includedir_server)'

 uninstall-data:
     rm -f $(addprefix '$(DESTDIR)$(datadir)/extension'/, $(notdir $(DATA)))
+    rm -f $(addprefix '$(DESTDIR)$(includedir_server)'/, plpython.h plpy_util.h)

 .PHONY: install-data uninstall-data


-ifeq ($(python_majorversion),3)
-# Adjust regression tests for Python 3 compatibility
-#
-# Mention those regression test files that need to be mangled in the
-# variable REGRESS_PLPYTHON3_MANGLE.  They will be copied to a
-# subdirectory python3/ and have their Python syntax and other bits
-# adjusted to work with Python 3.
-
-# Note that the order of the tests needs to be preserved in this
-# expression.
-REGRESS := $(foreach test,$(REGRESS),$(if $(filter $(test),$(REGRESS_PLPYTHON3_MANGLE)),python3/$(test),$(test)))
-
-.PHONY: pgregress-python3-mangle
-pgregress-python3-mangle:
-    $(MKDIR_P) sql/python3 expected/python3 results/python3
-    for file in $(patsubst %,$(srcdir)/sql/%.sql,$(REGRESS_PLPYTHON3_MANGLE)) $(patsubst
%,$(srcdir)/expected/%*.out,$(REGRESS_PLPYTHON3_MANGLE));do \ 
-      sed -e 's/except \([[:alpha:]][[:alpha:].]*\), *\([[:alpha:]][[:alpha:]]*\):/except \1 as \2:/g' \
-          -e "s/<type 'exceptions\.\([[:alpha:]]*\)'>/<class '\1'>/g" \
-          -e "s/<type 'long'>/<class 'int'>/g" \
-          -e "s/\([0-9][0-9]*\)L/\1/g" \
-          -e 's/\([ [{]\)u"/\1"/g' \
-          -e "s/\([ [{]\)u'/\1'/g" \
-          -e "s/def next/def __next__/g" \
-          -e "s/LANGUAGE plpythonu/LANGUAGE plpython3u/g" \
-          -e "s/LANGUAGE plpython2u/LANGUAGE plpython3u/g" \
-          -e "s/EXTENSION plpythonu/EXTENSION plpython3u/g" \
-          -e "s/EXTENSION plpython2u/EXTENSION plpython3u/g" \
-        $$file >`echo $$file | sed 's,^.*/\([^/][^/]*/\)\([^/][^/]*\)$$,\1python3/\2,'` || exit; \
-    done
-
-check installcheck: pgregress-python3-mangle
-
-pg_regress_clean_files += sql/python3/ expected/python3/ results/python3/
-
-endif # Python 3
+include $(srcdir)/regress-python3-mangle.mk


 check: all submake
diff --git a/src/pl/plpython/plpy_main.c b/src/pl/plpython/plpy_main.c
index 0dad843..10cb623 100644
--- a/src/pl/plpython/plpy_main.c
+++ b/src/pl/plpython/plpy_main.c
@@ -283,6 +283,7 @@

     MemSet(&proc, 0, sizeof(PLyProcedure));
     proc.pyname = PLy_strdup("__plpython_inline_block");
+    proc.langid = codeblock->langOid;
     proc.result.out.d.typoid = VOIDOID;

     /*
diff --git a/src/pl/plpython/plpy_procedure.c b/src/pl/plpython/plpy_procedure.c
index 5007e77..f8ee305 100644
--- a/src/pl/plpython/plpy_procedure.c
+++ b/src/pl/plpython/plpy_procedure.c
@@ -166,6 +166,7 @@
     for (i = 0; i < FUNC_MAX_ARGS; i++)
         PLy_typeinfo_init(&proc->args[i]);
     proc->nargs = 0;
+    proc->langid = procStruct->prolang;
     proc->code = proc->statics = NULL;
     proc->globals = NULL;
     proc->is_setof = procStruct->proretset;
@@ -220,7 +221,7 @@
             else
             {
                 /* do the real work */
-                PLy_output_datum_func(&proc->result, rvTypeTup);
+                PLy_output_datum_func(&proc->result, rvTypeTup, proc->langid);
             }

             ReleaseSysCache(rvTypeTup);
@@ -294,7 +295,8 @@
                     default:
                         PLy_input_datum_func(&(proc->args[pos]),
                                              types[i],
-                                             argTypeTup);
+                                             argTypeTup,
+                                             proc->langid);
                         break;
                 }

diff --git a/src/pl/plpython/plpy_procedure.h b/src/pl/plpython/plpy_procedure.h
index f1c8510..9776e7d 100644
--- a/src/pl/plpython/plpy_procedure.h
+++ b/src/pl/plpython/plpy_procedure.h
@@ -27,6 +27,7 @@ typedef struct PLyProcedure
     char      **argnames;        /* Argument names */
     PLyTypeInfo args[FUNC_MAX_ARGS];
     int            nargs;
+    Oid            langid;            /* OID of plpython pg_language entry */
     PyObject   *code;            /* compiled procedure code */
     PyObject   *statics;        /* data saved across calls, local scope */
     PyObject   *globals;        /* data saved across calls, global scope */
diff --git a/src/pl/plpython/plpy_spi.c b/src/pl/plpython/plpy_spi.c
index c9182eb..2fa255d 100644
--- a/src/pl/plpython/plpy_spi.c
+++ b/src/pl/plpython/plpy_spi.c
@@ -76,6 +76,7 @@
     PG_TRY();
     {
         int            i;
+        PLyExecutionContext *exec_ctx = PLy_current_execution_context();

         /*
          * the other loop might throw an exception, if PLyTypeInfo member
@@ -131,7 +132,7 @@
             plan->types[i] = typeId;
             typeStruct = (Form_pg_type) GETSTRUCT(typeTup);
             if (typeStruct->typtype != TYPTYPE_COMPOSITE)
-                PLy_output_datum_func(&plan->args[i], typeTup);
+                PLy_output_datum_func(&plan->args[i], typeTup, exec_ctx->curr_proc->langid);
             else
                 ereport(ERROR,
                         (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
diff --git a/src/pl/plpython/plpy_typeio.c b/src/pl/plpython/plpy_typeio.c
index 8f2367d..f670ebe 100644
--- a/src/pl/plpython/plpy_typeio.c
+++ b/src/pl/plpython/plpy_typeio.c
@@ -28,8 +28,8 @@


 /* I/O function caching */
-static void PLy_input_datum_func2(PLyDatumToOb *arg, Oid typeOid, HeapTuple typeTup);
-static void PLy_output_datum_func2(PLyObToDatum *arg, HeapTuple typeTup);
+static void PLy_input_datum_func2(PLyDatumToOb *arg, Oid typeOid, HeapTuple typeTup, Oid langid);
+static void PLy_output_datum_func2(PLyObToDatum *arg, HeapTuple typeTup, Oid langid);

 /* conversion from Datums to Python objects */
 static PyObject *PLyBool_FromBool(PLyDatumToOb *arg, Datum d);
@@ -42,6 +42,7 @@
 static PyObject *PLyLong_FromOid(PLyDatumToOb *arg, Datum d);
 static PyObject *PLyBytes_FromBytea(PLyDatumToOb *arg, Datum d);
 static PyObject *PLyString_FromDatum(PLyDatumToOb *arg, Datum d);
+static PyObject *PLyObject_FromTransform(PLyDatumToOb *arg, Datum d);
 static PyObject *PLyList_FromArray(PLyDatumToOb *arg, Datum d);

 /* conversion from Python objects to Datums */
@@ -49,6 +50,7 @@
 static Datum PLyObject_ToBytea(PLyObToDatum *arg, int32 typmod, PyObject *plrv);
 static Datum PLyObject_ToComposite(PLyObToDatum *arg, int32 typmod, PyObject *plrv);
 static Datum PLyObject_ToDatum(PLyObToDatum *arg, int32 typmod, PyObject *plrv);
+static Datum PLyObject_ToTransform(PLyObToDatum *arg, int32 typmod, PyObject *plrv);
 static Datum PLySequence_ToArray(PLyObToDatum *arg, int32 typmod, PyObject *plrv);

 /* conversion from Python objects to composite Datums (used by triggers and SRFs) */
@@ -101,27 +103,28 @@
  * PostgreSQL, and vice versa.
  */
 void
-PLy_input_datum_func(PLyTypeInfo *arg, Oid typeOid, HeapTuple typeTup)
+PLy_input_datum_func(PLyTypeInfo *arg, Oid typeOid, HeapTuple typeTup, Oid langid)
 {
     if (arg->is_rowtype > 0)
         elog(ERROR, "PLyTypeInfo struct is initialized for Tuple");
     arg->is_rowtype = 0;
-    PLy_input_datum_func2(&(arg->in.d), typeOid, typeTup);
+    PLy_input_datum_func2(&(arg->in.d), typeOid, typeTup, langid);
 }

 void
-PLy_output_datum_func(PLyTypeInfo *arg, HeapTuple typeTup)
+PLy_output_datum_func(PLyTypeInfo *arg, HeapTuple typeTup, Oid langid)
 {
     if (arg->is_rowtype > 0)
         elog(ERROR, "PLyTypeInfo struct is initialized for a Tuple");
     arg->is_rowtype = 0;
-    PLy_output_datum_func2(&(arg->out.d), typeTup);
+    PLy_output_datum_func2(&(arg->out.d), typeTup, langid);
 }

 void
 PLy_input_tuple_funcs(PLyTypeInfo *arg, TupleDesc desc)
 {
     int            i;
+    PLyExecutionContext *exec_ctx = PLy_current_execution_context();

     if (arg->is_rowtype == 0)
         elog(ERROR, "PLyTypeInfo struct is initialized for a Datum");
@@ -180,7 +183,8 @@

         PLy_input_datum_func2(&(arg->in.r.atts[i]),
                               desc->attrs[i]->atttypid,
-                              typeTup);
+                              typeTup,
+                              exec_ctx->curr_proc->langid);

         ReleaseSysCache(typeTup);
     }
@@ -190,6 +194,7 @@
 PLy_output_tuple_funcs(PLyTypeInfo *arg, TupleDesc desc)
 {
     int            i;
+    PLyExecutionContext *exec_ctx = PLy_current_execution_context();

     if (arg->is_rowtype == 0)
         elog(ERROR, "PLyTypeInfo struct is initialized for a Datum");
@@ -242,7 +247,7 @@
             elog(ERROR, "cache lookup failed for type %u",
                  desc->attrs[i]->atttypid);

-        PLy_output_datum_func2(&(arg->out.r.atts[i]), typeTup);
+        PLy_output_datum_func2(&(arg->out.r.atts[i]), typeTup, exec_ctx->curr_proc->langid);

         ReleaseSysCache(typeTup);
     }
@@ -361,10 +366,12 @@
 }

 static void
-PLy_output_datum_func2(PLyObToDatum *arg, HeapTuple typeTup)
+PLy_output_datum_func2(PLyObToDatum *arg, HeapTuple typeTup, Oid langid)
 {
     Form_pg_type typeStruct = (Form_pg_type) GETSTRUCT(typeTup);
     Oid            element_type;
+    Oid            base_type;
+    Oid            funcid;

     perm_fmgr_info(typeStruct->typinput, &arg->typfunc);
     arg->typoid = HeapTupleGetOid(typeTup);
@@ -373,12 +380,24 @@
     arg->typbyval = typeStruct->typbyval;

     element_type = get_element_type(arg->typoid);
+    base_type = getBaseType(element_type ? element_type : arg->typoid);

     /*
      * Select a conversion function to convert Python objects to PostgreSQL
-     * datums.    Most data types can go through the generic function.
+     * datums.
      */
-    switch (getBaseType(element_type ? element_type : arg->typoid))
+
+    if ((funcid = get_transform_tosql(base_type, langid)))
+    {
+        arg->func = PLyObject_ToTransform;
+        perm_fmgr_info(funcid, &arg->typtransform);
+    }
+    else if (typeStruct->typtype == TYPTYPE_COMPOSITE)
+    {
+        arg->func = PLyObject_ToComposite;
+    }
+    else
+    switch (base_type)
     {
         case BOOLOID:
             arg->func = PLyObject_ToBool;
@@ -391,12 +410,6 @@
             break;
     }

-    /* Composite types need their own input routine, though */
-    if (typeStruct->typtype == TYPTYPE_COMPOSITE)
-    {
-        arg->func = PLyObject_ToComposite;
-    }
-
     if (element_type)
     {
         char        dummy_delim;
@@ -411,6 +424,7 @@

         arg->elm = PLy_malloc0(sizeof(*arg->elm));
         arg->elm->func = arg->func;
+        arg->elm->typtransform = arg->typtransform;
         arg->func = PLySequence_ToArray;

         arg->elm->typoid = element_type;
@@ -423,10 +437,12 @@
 }

 static void
-PLy_input_datum_func2(PLyDatumToOb *arg, Oid typeOid, HeapTuple typeTup)
+PLy_input_datum_func2(PLyDatumToOb *arg, Oid typeOid, HeapTuple typeTup, Oid langid)
 {
     Form_pg_type typeStruct = (Form_pg_type) GETSTRUCT(typeTup);
-    Oid            element_type = get_element_type(typeOid);
+    Oid            element_type;
+    Oid            base_type;
+    Oid            funcid;

     /* Get the type's conversion information */
     perm_fmgr_info(typeStruct->typoutput, &arg->typfunc);
@@ -438,7 +454,17 @@
     arg->typalign = typeStruct->typalign;

     /* Determine which kind of Python object we will convert to */
-    switch (getBaseType(element_type ? element_type : typeOid))
+
+    element_type = get_element_type(typeOid);
+    base_type = getBaseType(element_type ? element_type : typeOid);
+
+    if ((funcid = get_transform_fromsql(base_type, langid)))
+    {
+        arg->func = PLyObject_FromTransform;
+        perm_fmgr_info(funcid, &arg->typtransform);
+    }
+    else
+    switch (base_type)
     {
         case BOOLOID:
             arg->func = PLyBool_FromBool;
@@ -479,6 +505,7 @@

         arg->elm = PLy_malloc0(sizeof(*arg->elm));
         arg->elm->func = arg->func;
+        arg->elm->typtransform = arg->typtransform;
         arg->func = PLyList_FromArray;
         arg->elm->typoid = element_type;
         arg->elm->typmod = -1;
@@ -569,14 +596,22 @@
 static PyObject *
 PLyString_FromDatum(PLyDatumToOb *arg, Datum d)
 {
-    char       *x = OutputFunctionCall(&arg->typfunc, d);
-    PyObject   *r = PyString_FromString(x);
+    char       *x;
+    PyObject   *r;

+    x = OutputFunctionCall(&arg->typfunc, d);
+    r = PyString_FromString(x);
     pfree(x);
     return r;
 }

 static PyObject *
+PLyObject_FromTransform(PLyDatumToOb *arg, Datum d)
+{
+    return (PyObject *) DatumGetPointer(FunctionCall1(&arg->typtransform, d));
+}
+
+static PyObject *
 PLyList_FromArray(PLyDatumToOb *arg, Datum d)
 {
     ArrayType  *array = DatumGetArrayTypeP(d);
@@ -725,16 +760,15 @@


 /*
- * Generic conversion function: Convert PyObject to cstring and
- * cstring into PostgreSQL type.
+ * Convert Python object to C string in server encoding.
  */
-static Datum
-PLyObject_ToDatum(PLyObToDatum *arg, int32 typmod, PyObject *plrv)
+char *
+PLyObject_AsString(PyObject *plrv)
 {
-    PyObject   *volatile plrv_bo = NULL;
-    Datum        rv;
-
-    Assert(plrv != Py_None);
+    PyObject   *plrv_bo;
+    char       *plrv_sc;
+    size_t        plen;
+    size_t        slen;

     if (PyUnicode_Check(plrv))
         plrv_bo = PLyUnicode_Bytes(plrv);
@@ -752,36 +786,47 @@
     if (!plrv_bo)
         PLy_elog(ERROR, "could not create string representation of Python object");

-    PG_TRY();
-    {
-        char       *plrv_sc = PyBytes_AsString(plrv_bo);
-        size_t        plen = PyBytes_Size(plrv_bo);
-        size_t        slen = strlen(plrv_sc);
-
-        if (slen < plen)
-            ereport(ERROR,
-                    (errcode(ERRCODE_DATATYPE_MISMATCH),
-                     errmsg("could not convert Python object into cstring: Python string representation appears to
containnull 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);
-        rv = InputFunctionCall(&arg->typfunc,
-                               plrv_sc,
-                               arg->typioparam,
-                               typmod);
-    }
-    PG_CATCH();
-    {
-        Py_XDECREF(plrv_bo);
-        PG_RE_THROW();
-    }
-    PG_END_TRY();
+    plrv_sc = pstrdup(PyBytes_AsString(plrv_bo));
+    plen = PyBytes_Size(plrv_bo);
+    slen = strlen(plrv_sc);

     Py_XDECREF(plrv_bo);

-    return rv;
+    if (slen < plen)
+        ereport(ERROR,
+                (errcode(ERRCODE_DATATYPE_MISMATCH),
+                 errmsg("could not convert Python object into cstring: Python string representation appears to contain
nullbytes"))); 
+    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 conversion function: Convert PyObject to cstring and
+ * cstring into PostgreSQL type.
+ */
+static Datum
+PLyObject_ToDatum(PLyObToDatum *arg, int32 typmod, PyObject *plrv)
+{
+    Assert(plrv != Py_None);
+
+    return InputFunctionCall(&arg->typfunc,
+                             PLyObject_AsString(plrv),
+                             arg->typioparam,
+                             typmod);
+}
+
+
+static Datum
+PLyObject_ToTransform(PLyObToDatum *arg, int32 typmod, PyObject *plrv)
+{
+    return FunctionCall1(&arg->typtransform, PointerGetDatum(plrv));
+}
+
+
 static Datum
 PLySequence_ToArray(PLyObToDatum *arg, int32 typmod, PyObject *plrv)
 {
@@ -831,12 +876,13 @@
 PLyString_ToComposite(PLyTypeInfo *info, TupleDesc desc, PyObject *string)
 {
     HeapTuple    typeTup;
+    PLyExecutionContext *exec_ctx = PLy_current_execution_context();

     typeTup = SearchSysCache1(TYPEOID, ObjectIdGetDatum(desc->tdtypeid));
     if (!HeapTupleIsValid(typeTup))
         elog(ERROR, "cache lookup failed for type %u", desc->tdtypeid);

-    PLy_output_datum_func2(&info->out.d, typeTup);
+    PLy_output_datum_func2(&info->out.d, typeTup, exec_ctx->curr_proc->langid);

     ReleaseSysCache(typeTup);
     ReleaseTupleDesc(desc);
diff --git a/src/pl/plpython/plpy_typeio.h b/src/pl/plpython/plpy_typeio.h
index 82e472a..b869aaa 100644
--- a/src/pl/plpython/plpy_typeio.h
+++ b/src/pl/plpython/plpy_typeio.h
@@ -17,6 +17,7 @@ typedef struct PLyDatumToOb
 {
     PLyDatumToObFunc func;
     FmgrInfo    typfunc;        /* The type's output function */
+    FmgrInfo    typtransform;    /* from-SQL transform */
     Oid            typoid;            /* The OID of the type */
     int32        typmod;            /* The typmod of the type */
     Oid            typioparam;
@@ -48,6 +49,7 @@ typedef struct PLyObToDatum
 {
     PLyObToDatumFunc func;
     FmgrInfo    typfunc;        /* The type's input function */
+    FmgrInfo    typtransform;    /* to-SQL transform */
     Oid            typoid;            /* The OID of the type */
     int32        typmod;            /* The typmod of the type */
     Oid            typioparam;
@@ -91,8 +93,8 @@ typedef struct PLyTypeInfo
 extern void PLy_typeinfo_init(PLyTypeInfo *arg);
 extern void PLy_typeinfo_dealloc(PLyTypeInfo *arg);

-extern void PLy_input_datum_func(PLyTypeInfo *arg, Oid typeOid, HeapTuple typeTup);
-extern void PLy_output_datum_func(PLyTypeInfo *arg, HeapTuple typeTup);
+extern void PLy_input_datum_func(PLyTypeInfo *arg, Oid typeOid, HeapTuple typeTup, Oid langid);
+extern void PLy_output_datum_func(PLyTypeInfo *arg, HeapTuple typeTup, Oid langid);

 extern void PLy_input_tuple_funcs(PLyTypeInfo *arg, TupleDesc desc);
 extern void PLy_output_tuple_funcs(PLyTypeInfo *arg, TupleDesc desc);
@@ -105,4 +107,7 @@ extern Datum PLyObject_ToCompositeDatum(PLyTypeInfo *info, TupleDesc desc, PyObj
 /* conversion from heap tuples to Python dictionaries */
 extern PyObject *PLyDict_FromTuple(PLyTypeInfo *info, HeapTuple tuple, TupleDesc desc);

+/* conversion from Python objects to C strings */
+extern 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
index 95cbba5..bfa09d8 100644
--- a/src/pl/plpython/plpy_util.c
+++ b/src/pl/plpython/plpy_util.c
@@ -144,22 +144,33 @@
  * unicode object.    Reference ownership is passed to the caller.
  */
 PyObject *
-PLyUnicode_FromString(const char *s)
+PLyUnicode_FromStringAndSize(const char *s, Py_ssize_t size)
 {
     char       *utf8string;
     PyObject   *o;

     utf8string = (char *) pg_do_encoding_conversion((unsigned char *) s,
-                                                    strlen(s),
+                                                    size,
                                                     GetDatabaseEncoding(),
                                                     PG_UTF8);

-    o = PyUnicode_FromString(utf8string);
-
-    if (utf8string != s)
+    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));
+}
+
 #endif   /* PY_MAJOR_VERSION >= 3 */
diff --git a/src/pl/plpython/plpy_util.h b/src/pl/plpython/plpy_util.h
index f93e837..4c29f9a 100644
--- a/src/pl/plpython/plpy_util.h
+++ b/src/pl/plpython/plpy_util.h
@@ -16,6 +16,7 @@ extern char *PLyUnicode_AsString(PyObject *unicode);

 #if PY_MAJOR_VERSION >= 3
 extern PyObject *PLyUnicode_FromString(const char *s);
+extern PyObject *PLyUnicode_FromStringAndSize(const char *s, Py_ssize_t size);
 #endif

 #endif   /* PLPY_UTIL_H */
diff --git a/src/pl/plpython/plpython.h b/src/pl/plpython/plpython.h
index 795231c..632b09a 100644
--- a/src/pl/plpython/plpython.h
+++ b/src/pl/plpython/plpython.h
@@ -91,6 +91,7 @@ typedef int Py_ssize_t;
 #define PyString_Check(x) 0
 #define PyString_AsString(x) PLyUnicode_AsString(x)
 #define PyString_FromString(x) PLyUnicode_FromString(x)
+#define PyString_FromStringAndSize(x, size) PLyUnicode_FromStringAndSize(x, size)
 #endif

 /*
diff --git a/src/pl/plpython/regress-python3-mangle.mk b/src/pl/plpython/regress-python3-mangle.mk
new file mode 100644
index 0000000..d2c7490
--- /dev/null
+++ b/src/pl/plpython/regress-python3-mangle.mk
@@ -0,0 +1,35 @@
+ifeq ($(python_majorversion),3)
+# Adjust regression tests for Python 3 compatibility
+#
+# Mention those regression test files that need to be mangled in the
+# variable REGRESS_PLPYTHON3_MANGLE.  They will be copied to a
+# subdirectory python3/ and have their Python syntax and other bits
+# adjusted to work with Python 3.
+
+# Note that the order of the tests needs to be preserved in this
+# expression.
+REGRESS := $(foreach test,$(REGRESS),$(if $(filter $(test),$(REGRESS_PLPYTHON3_MANGLE)),python3/$(test),$(test)))
+
+.PHONY: pgregress-python3-mangle
+pgregress-python3-mangle:
+    $(MKDIR_P) sql/python3 expected/python3 results/python3
+    for file in $(patsubst %,$(srcdir)/sql/%.sql,$(REGRESS_PLPYTHON3_MANGLE)) $(patsubst
%,$(srcdir)/expected/%*.out,$(REGRESS_PLPYTHON3_MANGLE));do \ 
+      sed -e 's/except \([[:alpha:]][[:alpha:].]*\), *\([[:alpha:]][[:alpha:]]*\):/except \1 as \2:/g' \
+          -e "s/<type 'exceptions\.\([[:alpha:]]*\)'>/<class '\1'>/g" \
+          -e "s/<type 'long'>/<class 'int'>/g" \
+          -e "s/\([0-9][0-9]*\)L/\1/g" \
+          -e 's/\([ [{]\)u"/\1"/g' \
+          -e "s/\([ [{]\)u'/\1'/g" \
+          -e "s/def next/def __next__/g" \
+          -e "s/LANGUAGE plpythonu/LANGUAGE plpython3u/g" \
+          -e "s/LANGUAGE plpython2u/LANGUAGE plpython3u/g" \
+          -e "s/EXTENSION \([^ ]*_\)*plpythonu/EXTENSION \1plpython3u/g" \
+          -e "s/EXTENSION \([^ ]*_\)*plpython2u/EXTENSION \1plpython3u/g" \
+        $$file >`echo $$file | sed 's,^.*/\([^/][^/]*/\)\([^/][^/]*\)$$,\1python3/\2,'` || exit; \
+    done
+
+check installcheck: pgregress-python3-mangle
+
+pg_regress_clean_files += sql/python3/ expected/python3/ results/python3/
+
+endif # Python 3
diff --git a/src/test/regress/expected/sanity_check.out b/src/test/regress/expected/sanity_check.out
index 432d39a..85fb3f7 100644
--- a/src/test/regress/expected/sanity_check.out
+++ b/src/test/regress/expected/sanity_check.out
@@ -126,6 +126,7 @@ SELECT relname, relhasindex
  pg_shseclabel           | t
  pg_statistic            | t
  pg_tablespace           | t
+ pg_transform            | t
  pg_trigger              | t
  pg_ts_config            | t
  pg_ts_config_map        | t
@@ -166,7 +167,7 @@ SELECT relname, relhasindex
  timetz_tbl              | f
  tinterval_tbl           | f
  varchar_tbl             | f
-(155 rows)
+(156 rows)

 --
 -- another sanity check: every system catalog that has OIDs should have

В списке pgsql-hackers по дате отправления:

Предыдущее
От: Mark Kirkwood
Дата:
Сообщение: Re: Clean switchover
Следующее
От: Amit Kapila
Дата:
Сообщение: Re: ALTER SYSTEM SET command to change postgresql.conf parameters (RE: Proposal for Allow postgresql.conf values to be changed via SQL [review])