Re: Allowing extensions to supply operator-/function-specific info - Mailing list pgsql-hackers

From Tom Lane
Subject Re: Allowing extensions to supply operator-/function-specific info
Date
Msg-id 6953.1548297558@sss.pgh.pa.us
Whole thread Raw
In response to Allowing extensions to supply operator-/function-specific info  (Tom Lane <tgl@sss.pgh.pa.us>)
Responses Re: Allowing extensions to supply operator-/function-specific info  (Tom Lane <tgl@sss.pgh.pa.us>)
List pgsql-hackers
I wrote:
> What I'm envisioning therefore is that we allow an auxiliary function to
> be attached to any operator or function that can provide functionality
> like this, and that we set things up so that the set of tasks that
> such functions can perform can be extended over time without SQL-level
> changes.

Here are some draft patches in pursuit of this goal.

0001 redefines the API for protransform functions, renames that pg_proc
column to prosupport, and likewise renames the existing transform
functions to be xxx_support.  There are no actual functionality changes
in this step.  I needed to reindent the existing code in the transform
functions, so for ease of review, the -review patch uses "git diff -b"
to suppress most of the reindentation diffs.  If you want to actually
apply the patch for testing, use the -apply version.

Possibly worth noting is that I chose to just remove
timestamp_zone_transform and timestamp_izone_transform, rather than
change them from one no-op state to another.  We left them in place in
commit c22ecc656 to avoid a catversion bump, but that argument no longer
applies, and there seems little likelihood that we'll need them soon.

0002 adds the ability to attach a support function via CREATE/ALTER
FUNCTION, and adds the necessary pg_dump and ruleutils support for that.
The only thing that's not pretty mechanical about that is that ALTER
FUNCTION needs the ability to replace a dependency on a previous
support function.  For that, we should use changeDependencyFor() ...
but there's a problem, which is that that function can't cope with
the case where the existing dependency is on a pinned object.
We'd left that unimplemented, arguing that it wasn't really necessary
for the existing usage of that function to change schema dependencies.
But it seems fairly likely that the case would occur for support
functions, so I went ahead and fixed changeDependencyFor() to handle
it.  That leads to a change in the alter_table regression test, which
was pedantically verifying that the limitation existed.

(We could alternatively leave out the ability to set this option in
ALTER FUNCTION, requiring people to use CREATE OR REPLACE FUNCTION
for it.  But I'm figuring that extension update scripts will want to
add support functions to existing functions, so it'd be tedious to not
be able to do it with a simple ALTER.)

0003 is where something useful happens.  It extends the API to allow
support functions to define the selectivity estimates, cost estimates,
and rowcount estimates (for set-returning functions) of their target
functions.  I can't overstate how important this is: it's retiring
technical debt that has been there for decades.  As proof of concept,
there is a quick hack in the regression tests that teaches the planner
to make accurate rowcount estimates for generate_series(int, int)
with constant or estimatable arguments.

There's a considerable amount of follow-up work that ought to happen
now to make use of these capabilities for places that have been
pain points in the past, such as generate_series() and unnest().
But I haven't touched that yet.

Still to be done is to provide an API responding to Paul's original
problem, i.e. allowing an extension to generate lossy index clauses
when one of its operators or functions appears in WHERE.  That's
going to be more complex than 0003 --- for one thing, I think I'd
like to try to refactor the existing hard-wired cases in indxpath.c
so that they live in datatype-specific support functions instead of
the core index code.

But first, I'd like to push forward with committing what I've got.
I think this is pretty damn compelling already, even if nothing
further got done for v12.  Is anybody interested in reviewing?

            regards, tom lane

diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index af4d062..6dd0700 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -5146,11 +5146,11 @@ SCRAM-SHA-256$<replaceable><iteration count></replaceable>:<replaceable>&l
      </row>

      <row>
-      <entry><structfield>protransform</structfield></entry>
+      <entry><structfield>prosupport</structfield></entry>
       <entry><type>regproc</type></entry>
       <entry><literal><link linkend="catalog-pg-proc"><structname>pg_proc</structname></link>.oid</literal></entry>
-      <entry>Calls to this function can be simplified by this other function
-       (see <xref linkend="xfunc-transform-functions"/>)</entry>
+      <entry>Optional planner support function for this function
+       (see <xref linkend="xfunc-optimization"/>)</entry>
      </row>

      <row>
diff --git a/doc/src/sgml/xfunc.sgml b/doc/src/sgml/xfunc.sgml
index e18272c..d70aa6e 100644
--- a/doc/src/sgml/xfunc.sgml
+++ b/doc/src/sgml/xfunc.sgml
@@ -3241,40 +3241,6 @@ CREATE FUNCTION make_array(anyelement) RETURNS anyarray
     </para>
    </sect2>

-   <sect2 id="xfunc-transform-functions">
-    <title>Transform Functions</title>
-
-    <para>
-     Some function calls can be simplified during planning based on
-     properties specific to the function.  For example,
-     <literal>int4mul(n, 1)</literal> could be simplified to just <literal>n</literal>.
-     To define such function-specific optimizations, write a
-     <firstterm>transform function</firstterm> and place its OID in the
-     <structfield>protransform</structfield> field of the primary function's
-     <structname>pg_proc</structname> entry.  The transform function must have the SQL
-     signature <literal>protransform(internal) RETURNS internal</literal>.  The
-     argument, actually <type>FuncExpr *</type>, is a dummy node representing a
-     call to the primary function.  If the transform function's study of the
-     expression tree proves that a simplified expression tree can substitute
-     for all possible concrete calls represented thereby, build and return
-     that simplified expression.  Otherwise, return a <literal>NULL</literal>
-     pointer (<emphasis>not</emphasis> a SQL null).
-    </para>
-
-    <para>
-     We make no guarantee that <productname>PostgreSQL</productname> will never call the
-     primary function in cases that the transform function could simplify.
-     Ensure rigorous equivalence between the simplified expression and an
-     actual call to the primary function.
-    </para>
-
-    <para>
-     Currently, this facility is not exposed to users at the SQL level
-     because of security concerns, so it is only practical to use for
-     optimizing built-in functions.
-    </para>
-   </sect2>
-
    <sect2>
     <title>Shared Memory and LWLocks</title>

@@ -3388,3 +3354,89 @@ if (!ptr)
    </sect2>

   </sect1>
+
+  <sect1 id="xfunc-optimization">
+   <title>Function Optimization Information</title>
+
+  <indexterm zone="xfunc-optimization">
+   <primary>optimization information</primary>
+   <secondary>for functions</secondary>
+  </indexterm>
+
+   <para>
+    By default, a function is just a <quote>black box</quote> that the
+    database system knows very little about the behavior of.  However,
+    that means that queries using the function may be executed much less
+    efficiently than they could be.  It is possible to supply additional
+    knowledge that helps the planner optimize function calls.
+   </para>
+
+   <para>
+    Some basic facts can be supplied by declarative annotations provided in
+    the <xref linkend="sql-createfunction"/> command.  Most important of
+    these is the function's <link linkend="xfunc-volatility">volatility
+    category</link> (<literal>IMMUTABLE</literal>, <literal>STABLE</literal>,
+    or <literal>VOLATILE</literal>); one should always be careful to
+    specify this correctly when defining a function.
+    The parallel safety property (<literal>PARALLEL
+    UNSAFE</literal>, <literal>PARALLEL RESTRICTED</literal>, or
+    <literal>PARALLEL SAFE</literal>) must also be specified if you hope
+    to use the function in parallelized queries.
+    It can also be useful to specify the function's estimated execution
+    cost, and/or the number of rows a set-returning function is estimated
+    to return.  However, the declarative way of specifying those two
+    facts only allows specifying a constant value, which is often
+    inadequate.
+   </para>
+
+   <para>
+    It is also possible to attach a <firstterm>planner support
+    function</firstterm> to a SQL-callable function (called
+    its <firstterm>target function</firstterm>), and thereby provide
+    knowledge about the target function that is too complex to be
+    represented declaratively.  Planner support functions have to be
+    written in C (although their target functions might not be), so this is
+    an advanced feature that relatively few people will use.
+   </para>
+
+   <para>
+    A planner support function must have the SQL signature
+<programlisting>
+supportfn(internal) returns internal
+</programlisting>
+    It is attached to its target function by specifying
+    the <literal>SUPPORT</literal> clause when creating the target function.
+   </para>
+
+   <para>
+    The details of the API for planner support functions can be found in
+    file <filename>src/include/nodes/supportnodes.h</filename> in the
+    <productname>PostgreSQL</productname> source code.  Here we provide
+    just an overview of what planner support functions can do.
+    The set of possible requests to a support function is extensible,
+    so more things might be possible in future versions.
+   </para>
+
+   <para>
+    Some function calls can be simplified during planning based on
+    properties specific to the function.  For example,
+    <literal>int4mul(n, 1)</literal> could be simplified to
+    just <literal>n</literal>.  This type of transformation can be
+    performed by a planner support function, by having it implement
+    the <literal>SupportRequestSimplify</literal> request type.
+    The support function will be called for each instance of its target
+    function found in a query parse tree.  If it finds that the particular
+    call can be simplified into some other form, it can build and return a
+    parse tree representing that expression.  This will automatically work
+    for operators based on the function, too — in the example just
+    given, <literal>n * 1</literal> would also be simplified to
+    <literal>n</literal>.
+    (But note that this is just an example; this particular
+    optimization is not actually performed by
+    standard <productname>PostgreSQL</productname>.)
+    We make no guarantee that <productname>PostgreSQL</productname> will
+    never call the target function in cases that the support function could
+    simplify.  Ensure rigorous equivalence between the simplified
+    expression and an actual execution of the target function.
+   </para>
+  </sect1>
diff --git a/doc/src/sgml/xoper.sgml b/doc/src/sgml/xoper.sgml
index 2f5560a..260e43c 100644
--- a/doc/src/sgml/xoper.sgml
+++ b/doc/src/sgml/xoper.sgml
@@ -78,6 +78,11 @@ SELECT (a + b) AS c FROM test_complex;
   <sect1 id="xoper-optimization">
    <title>Operator Optimization Information</title>

+  <indexterm zone="xoper-optimization">
+   <primary>optimization information</primary>
+   <secondary>for operators</secondary>
+  </indexterm>
+
    <para>
     A <productname>PostgreSQL</productname> operator definition can include
     several optional clauses that tell the system useful things about how
@@ -97,6 +102,13 @@ SELECT (a + b) AS c FROM test_complex;
     the ones that release &version; understands.
    </para>

+   <para>
+    It is also possible to attach a planner support function to the function
+    that underlies an operator, providing another way of telling the system
+    about the behavior of the operator.
+    See <xref linkend="xfunc-optimization"/> for more information.
+   </para>
+
    <sect2>
     <title><literal>COMMUTATOR</literal></title>

diff --git a/src/backend/catalog/pg_proc.c b/src/backend/catalog/pg_proc.c
index db78061..3a86f1e 100644
--- a/src/backend/catalog/pg_proc.c
+++ b/src/backend/catalog/pg_proc.c
@@ -319,7 +319,7 @@ ProcedureCreate(const char *procedureName,
     values[Anum_pg_proc_procost - 1] = Float4GetDatum(procost);
     values[Anum_pg_proc_prorows - 1] = Float4GetDatum(prorows);
     values[Anum_pg_proc_provariadic - 1] = ObjectIdGetDatum(variadicType);
-    values[Anum_pg_proc_protransform - 1] = ObjectIdGetDatum(InvalidOid);
+    values[Anum_pg_proc_prosupport - 1] = ObjectIdGetDatum(InvalidOid);
     values[Anum_pg_proc_prokind - 1] = CharGetDatum(prokind);
     values[Anum_pg_proc_prosecdef - 1] = BoolGetDatum(security_definer);
     values[Anum_pg_proc_proleakproof - 1] = BoolGetDatum(isLeakProof);
diff --git a/src/backend/optimizer/util/clauses.c b/src/backend/optimizer/util/clauses.c
index f0ef102..061a855 100644
--- a/src/backend/optimizer/util/clauses.c
+++ b/src/backend/optimizer/util/clauses.c
@@ -32,6 +32,7 @@
 #include "miscadmin.h"
 #include "nodes/makefuncs.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/supportnodes.h"
 #include "optimizer/clauses.h"
 #include "optimizer/cost.h"
 #include "optimizer/planmain.h"
@@ -4269,13 +4270,16 @@ simplify_function(Oid funcid, Oid result_type, int32 result_typmod,
                                 args, funcvariadic,
                                 func_tuple, context);

-    if (!newexpr && allow_non_const && OidIsValid(func_form->protransform))
+    if (!newexpr && allow_non_const && OidIsValid(func_form->prosupport))
     {
         /*
-         * Build a dummy FuncExpr node containing the simplified arg list.  We
-         * use this approach to present a uniform interface to the transform
-         * function regardless of how the function is actually being invoked.
+         * Build a SupportRequestSimplify node to pass to the support
+         * function, pointing to a dummy FuncExpr node containing the
+         * simplified arg list.  We use this approach to present a uniform
+         * interface to the support function regardless of how the target
+         * function is actually being invoked.
          */
+        SupportRequestSimplify req;
         FuncExpr    fexpr;

         fexpr.xpr.type = T_FuncExpr;
@@ -4289,9 +4293,16 @@ simplify_function(Oid funcid, Oid result_type, int32 result_typmod,
         fexpr.args = args;
         fexpr.location = -1;

+        req.type = T_SupportRequestSimplify;
+        req.root = context->root;
+        req.fcall = &fexpr;
+
         newexpr = (Expr *)
-            DatumGetPointer(OidFunctionCall1(func_form->protransform,
-                                             PointerGetDatum(&fexpr)));
+            DatumGetPointer(OidFunctionCall1(func_form->prosupport,
+                                             PointerGetDatum(&req)));
+
+        /* catch a possible API misunderstanding */
+        Assert(newexpr != (Expr *) &fexpr);
     }

     if (!newexpr && allow_non_const)
diff --git a/src/backend/utils/adt/date.c b/src/backend/utils/adt/date.c
index 3810e4a..cf5a1c6 100644
--- a/src/backend/utils/adt/date.c
+++ b/src/backend/utils/adt/date.c
@@ -24,6 +24,7 @@
 #include "access/xact.h"
 #include "libpq/pqformat.h"
 #include "miscadmin.h"
+#include "nodes/supportnodes.h"
 #include "parser/scansup.h"
 #include "utils/array.h"
 #include "utils/builtins.h"
@@ -1341,15 +1342,25 @@ make_time(PG_FUNCTION_ARGS)
 }


-/* time_transform()
- * Flatten calls to time_scale() and timetz_scale() that solely represent
- * increases in allowed precision.
+/* time_support()
+ *
+ * Planner support function for the time_scale() and timetz_scale()
+ * length coercion functions (we need not distinguish them here).
  */
 Datum
-time_transform(PG_FUNCTION_ARGS)
+time_support(PG_FUNCTION_ARGS)
 {
-    PG_RETURN_POINTER(TemporalTransform(MAX_TIME_PRECISION,
-                                        (Node *) PG_GETARG_POINTER(0)));
+    Node       *rawreq = (Node *) PG_GETARG_POINTER(0);
+    Node       *ret = NULL;
+
+    if (IsA(rawreq, SupportRequestSimplify))
+    {
+        SupportRequestSimplify *req = (SupportRequestSimplify *) rawreq;
+
+        ret = TemporalSimplify(MAX_TIME_PRECISION, (Node *) req->fcall);
+    }
+
+    PG_RETURN_POINTER(ret);
 }

 /* time_scale()
diff --git a/src/backend/utils/adt/datetime.c b/src/backend/utils/adt/datetime.c
index 61dbd05..0068e71 100644
--- a/src/backend/utils/adt/datetime.c
+++ b/src/backend/utils/adt/datetime.c
@@ -4462,16 +4462,23 @@ CheckDateTokenTables(void)
 }

 /*
- * Common code for temporal protransform functions.  Types time, timetz,
- * timestamp and timestamptz each have a range of allowed precisions.  An
- * unspecified precision is rigorously equivalent to the highest specifiable
- * precision.
+ * Common code for temporal prosupport functions: simplify, if possible,
+ * a call to a temporal type's length-coercion function.
+ *
+ * Types time, timetz, timestamp and timestamptz each have a range of allowed
+ * precisions.  An unspecified precision is rigorously equivalent to the
+ * highest specifiable precision.  We can replace the function call with a
+ * no-op RelabelType if it is coercing to the same or higher precision as the
+ * input is known to have.
+ *
+ * The input Node is always a FuncExpr, but to reduce the #include footprint
+ * of datetime.h, we declare it as Node *.
  *
  * Note: timestamp_scale throws an error when the typmod is out of range, but
  * we can't get there from a cast: our typmodin will have caught it already.
  */
 Node *
-TemporalTransform(int32 max_precis, Node *node)
+TemporalSimplify(int32 max_precis, Node *node)
 {
     FuncExpr   *expr = castNode(FuncExpr, node);
     Node       *ret = NULL;
diff --git a/src/backend/utils/adt/numeric.c b/src/backend/utils/adt/numeric.c
index 45cd1a0..1c9deeb 100644
--- a/src/backend/utils/adt/numeric.c
+++ b/src/backend/utils/adt/numeric.c
@@ -34,6 +34,7 @@
 #include "libpq/pqformat.h"
 #include "miscadmin.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/supportnodes.h"
 #include "utils/array.h"
 #include "utils/builtins.h"
 #include "utils/float.h"
@@ -890,19 +891,25 @@ numeric_send(PG_FUNCTION_ARGS)


 /*
- * numeric_transform() -
+ * numeric_support()
  *
- * Flatten calls to numeric's length coercion function that solely represent
- * increases in allowable precision.  Scale changes mutate every datum, so
- * they are unoptimizable.  Some values, e.g. 1E-1001, can only fit into an
- * unconstrained numeric, so a change from an unconstrained numeric to any
- * constrained numeric is also unoptimizable.
+ * Planner support function for the numeric() length coercion function.
+ *
+ * Flatten calls that solely represent increases in allowable precision.
+ * Scale changes mutate every datum, so they are unoptimizable.  Some values,
+ * e.g. 1E-1001, can only fit into an unconstrained numeric, so a change from
+ * an unconstrained numeric to any constrained numeric is also unoptimizable.
  */
 Datum
-numeric_transform(PG_FUNCTION_ARGS)
+numeric_support(PG_FUNCTION_ARGS)
 {
-    FuncExpr   *expr = castNode(FuncExpr, PG_GETARG_POINTER(0));
+    Node       *rawreq = (Node *) PG_GETARG_POINTER(0);
     Node       *ret = NULL;
+
+    if (IsA(rawreq, SupportRequestSimplify))
+    {
+        SupportRequestSimplify *req = (SupportRequestSimplify *) rawreq;
+        FuncExpr   *expr = req->fcall;
         Node       *typmod;

         Assert(list_length(expr->args) >= 2);
@@ -920,16 +927,18 @@ numeric_transform(PG_FUNCTION_ARGS)
             int32        new_precision = (new_typmod - VARHDRSZ) >> 16 & 0xffff;

             /*
-         * If new_typmod < VARHDRSZ, the destination is unconstrained; that's
-         * always OK.  If old_typmod >= VARHDRSZ, the source is constrained,
-         * and we're OK if the scale is unchanged and the precision is not
-         * decreasing.  See further notes in function header comment.
+             * If new_typmod < VARHDRSZ, the destination is unconstrained;
+             * that's always OK.  If old_typmod >= VARHDRSZ, the source is
+             * constrained, and we're OK if the scale is unchanged and the
+             * precision is not decreasing.  See further notes in function
+             * header comment.
              */
             if (new_typmod < (int32) VARHDRSZ ||
                 (old_typmod >= (int32) VARHDRSZ &&
                  new_scale == old_scale && new_precision >= old_precision))
                 ret = relabel_to_typmod(source, new_typmod);
         }
+    }

     PG_RETURN_POINTER(ret);
 }
diff --git a/src/backend/utils/adt/timestamp.c b/src/backend/utils/adt/timestamp.c
index 7befb6a..e0ef2f7 100644
--- a/src/backend/utils/adt/timestamp.c
+++ b/src/backend/utils/adt/timestamp.c
@@ -29,6 +29,7 @@
 #include "miscadmin.h"
 #include "nodes/makefuncs.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/supportnodes.h"
 #include "parser/scansup.h"
 #include "utils/array.h"
 #include "utils/builtins.h"
@@ -297,15 +298,26 @@ timestamptypmodout(PG_FUNCTION_ARGS)
 }


-/* timestamp_transform()
- * Flatten calls to timestamp_scale() and timestamptz_scale() that solely
- * represent increases in allowed precision.
+/*
+ * timestamp_support()
+ *
+ * Planner support function for the timestamp_scale() and timestamptz_scale()
+ * length coercion functions (we need not distinguish them here).
  */
 Datum
-timestamp_transform(PG_FUNCTION_ARGS)
+timestamp_support(PG_FUNCTION_ARGS)
 {
-    PG_RETURN_POINTER(TemporalTransform(MAX_TIMESTAMP_PRECISION,
-                                        (Node *) PG_GETARG_POINTER(0)));
+    Node       *rawreq = (Node *) PG_GETARG_POINTER(0);
+    Node       *ret = NULL;
+
+    if (IsA(rawreq, SupportRequestSimplify))
+    {
+        SupportRequestSimplify *req = (SupportRequestSimplify *) rawreq;
+
+        ret = TemporalSimplify(MAX_TIMESTAMP_PRECISION, (Node *) req->fcall);
+    }
+
+    PG_RETURN_POINTER(ret);
 }

 /* timestamp_scale()
@@ -1235,16 +1247,25 @@ intervaltypmodleastfield(int32 typmod)
 }


-/* interval_transform()
+/*
+ * interval_support()
+ *
+ * Planner support function for interval_scale().
+ *
  * Flatten superfluous calls to interval_scale().  The interval typmod is
  * complex to permit accepting and regurgitating all SQL standard variations.
  * For truncation purposes, it boils down to a single, simple granularity.
  */
 Datum
-interval_transform(PG_FUNCTION_ARGS)
+interval_support(PG_FUNCTION_ARGS)
 {
-    FuncExpr   *expr = castNode(FuncExpr, PG_GETARG_POINTER(0));
+    Node       *rawreq = (Node *) PG_GETARG_POINTER(0);
     Node       *ret = NULL;
+
+    if (IsA(rawreq, SupportRequestSimplify))
+    {
+        SupportRequestSimplify *req = (SupportRequestSimplify *) rawreq;
+        FuncExpr   *expr = req->fcall;
         Node       *typmod;

         Assert(list_length(expr->args) >= 2);
@@ -1277,9 +1298,9 @@ interval_transform(PG_FUNCTION_ARGS)

                 /*
                  * Cast is a no-op if least field stays the same or decreases
-             * while precision stays the same or increases.  But precision,
-             * which is to say, sub-second precision, only affects ranges that
-             * include SECOND.
+                 * while precision stays the same or increases.  But
+                 * precision, which is to say, sub-second precision, only
+                 * affects ranges that include SECOND.
                  */
                 noop = (new_least_field <= old_least_field) &&
                     (old_least_field > 0 /* SECOND */ ||
@@ -1289,6 +1310,7 @@ interval_transform(PG_FUNCTION_ARGS)
             if (noop)
                 ret = relabel_to_typmod(source, new_typmod);
         }
+    }

     PG_RETURN_POINTER(ret);
 }
@@ -1359,7 +1381,7 @@ AdjustIntervalForTypmod(Interval *interval, int32 typmod)
          * can't do it consistently.  (We cannot enforce a range limit on the
          * highest expected field, since we do not have any equivalent of
          * SQL's <interval leading field precision>.)  If we ever decide to
-         * revisit this, interval_transform will likely require adjusting.
+         * revisit this, interval_support will likely require adjusting.
          *
          * Note: before PG 8.4 we interpreted a limited set of fields as
          * actually causing a "modulo" operation on a given value, potentially
@@ -5020,18 +5042,6 @@ interval_part(PG_FUNCTION_ARGS)
 }


-/* timestamp_zone_transform()
- * The original optimization here caused problems by relabeling Vars that
- * could be matched to index entries.  It might be possible to resurrect it
- * at some point by teaching the planner to be less cavalier with RelabelType
- * nodes, but that will take careful analysis.
- */
-Datum
-timestamp_zone_transform(PG_FUNCTION_ARGS)
-{
-    PG_RETURN_POINTER(NULL);
-}
-
 /*    timestamp_zone()
  *    Encode timestamp type with specified time zone.
  *    This function is just timestamp2timestamptz() except instead of
@@ -5125,18 +5135,6 @@ timestamp_zone(PG_FUNCTION_ARGS)
     PG_RETURN_TIMESTAMPTZ(result);
 }

-/* timestamp_izone_transform()
- * The original optimization here caused problems by relabeling Vars that
- * could be matched to index entries.  It might be possible to resurrect it
- * at some point by teaching the planner to be less cavalier with RelabelType
- * nodes, but that will take careful analysis.
- */
-Datum
-timestamp_izone_transform(PG_FUNCTION_ARGS)
-{
-    PG_RETURN_POINTER(NULL);
-}
-
 /* timestamp_izone()
  * Encode timestamp type with specified time interval as time zone.
  */
diff --git a/src/backend/utils/adt/varbit.c b/src/backend/utils/adt/varbit.c
index 1585da0..fdcc620 100644
--- a/src/backend/utils/adt/varbit.c
+++ b/src/backend/utils/adt/varbit.c
@@ -20,6 +20,7 @@
 #include "common/int.h"
 #include "libpq/pqformat.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/supportnodes.h"
 #include "utils/array.h"
 #include "utils/builtins.h"
 #include "utils/varbit.h"
@@ -672,16 +673,24 @@ varbit_send(PG_FUNCTION_ARGS)
 }

 /*
- * varbit_transform()
- * Flatten calls to varbit's length coercion function that set the new maximum
- * length >= the previous maximum length.  We can ignore the isExplicit
- * argument, since that only affects truncation cases.
+ * varbit_support()
+ *
+ * Planner support function for the varbit() length coercion function.
+ *
+ * Currently, the only interesting thing we can do is flatten calls that set
+ * the new maximum length >= the previous maximum length.  We can ignore the
+ * isExplicit argument, since that only affects truncation cases.
  */
 Datum
-varbit_transform(PG_FUNCTION_ARGS)
+varbit_support(PG_FUNCTION_ARGS)
 {
-    FuncExpr   *expr = castNode(FuncExpr, PG_GETARG_POINTER(0));
+    Node       *rawreq = (Node *) PG_GETARG_POINTER(0);
     Node       *ret = NULL;
+
+    if (IsA(rawreq, SupportRequestSimplify))
+    {
+        SupportRequestSimplify *req = (SupportRequestSimplify *) rawreq;
+        FuncExpr   *expr = req->fcall;
         Node       *typmod;

         Assert(list_length(expr->args) >= 2);
@@ -699,6 +708,7 @@ varbit_transform(PG_FUNCTION_ARGS)
             if (new_max <= 0 || (old_max > 0 && old_max <= new_max))
                 ret = relabel_to_typmod(source, new_typmod);
         }
+    }

     PG_RETURN_POINTER(ret);
 }
diff --git a/src/backend/utils/adt/varchar.c b/src/backend/utils/adt/varchar.c
index 5cf927e..c866af0 100644
--- a/src/backend/utils/adt/varchar.c
+++ b/src/backend/utils/adt/varchar.c
@@ -21,6 +21,7 @@
 #include "catalog/pg_type.h"
 #include "libpq/pqformat.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/supportnodes.h"
 #include "utils/array.h"
 #include "utils/builtins.h"
 #include "utils/varlena.h"
@@ -547,16 +548,24 @@ varcharsend(PG_FUNCTION_ARGS)


 /*
- * varchar_transform()
- * Flatten calls to varchar's length coercion function that set the new maximum
- * length >= the previous maximum length.  We can ignore the isExplicit
- * argument, since that only affects truncation cases.
+ * varchar_support()
+ *
+ * Planner support function for the varchar() length coercion function.
+ *
+ * Currently, the only interesting thing we can do is flatten calls that set
+ * the new maximum length >= the previous maximum length.  We can ignore the
+ * isExplicit argument, since that only affects truncation cases.
  */
 Datum
-varchar_transform(PG_FUNCTION_ARGS)
+varchar_support(PG_FUNCTION_ARGS)
 {
-    FuncExpr   *expr = castNode(FuncExpr, PG_GETARG_POINTER(0));
+    Node       *rawreq = (Node *) PG_GETARG_POINTER(0);
     Node       *ret = NULL;
+
+    if (IsA(rawreq, SupportRequestSimplify))
+    {
+        SupportRequestSimplify *req = (SupportRequestSimplify *) rawreq;
+        FuncExpr   *expr = req->fcall;
         Node       *typmod;

         Assert(list_length(expr->args) >= 2);
@@ -574,6 +583,7 @@ varchar_transform(PG_FUNCTION_ARGS)
             if (new_typmod < 0 || (old_typmod >= 0 && old_max <= new_max))
                 ret = relabel_to_typmod(source, new_typmod);
         }
+    }

     PG_RETURN_POINTER(ret);
 }
diff --git a/src/bin/pg_dump/t/002_pg_dump.pl b/src/bin/pg_dump/t/002_pg_dump.pl
index 245fcbf..4ff358a 100644
--- a/src/bin/pg_dump/t/002_pg_dump.pl
+++ b/src/bin/pg_dump/t/002_pg_dump.pl
@@ -1883,9 +1883,9 @@ my %tests = (
     'CREATE TRANSFORM FOR int' => {
         create_order => 34,
         create_sql =>
-          'CREATE TRANSFORM FOR int LANGUAGE SQL (FROM SQL WITH FUNCTION varchar_transform(internal), TO SQL WITH
FUNCTIONint4recv(internal));', 
+          'CREATE TRANSFORM FOR int LANGUAGE SQL (FROM SQL WITH FUNCTION varchar_support(internal), TO SQL WITH
FUNCTIONint4recv(internal));', 
         regexp =>
-          qr/CREATE TRANSFORM FOR integer LANGUAGE sql \(FROM SQL WITH FUNCTION
pg_catalog\.varchar_transform\(internal\),TO SQL WITH FUNCTION pg_catalog\.int4recv\(internal\)\);/m, 
+          qr/CREATE TRANSFORM FOR integer LANGUAGE sql \(FROM SQL WITH FUNCTION
pg_catalog\.varchar_support\(internal\),TO SQL WITH FUNCTION pg_catalog\.int4recv\(internal\)\);/m, 
         like => { %full_runs, section_pre_data => 1, },
     },

@@ -2880,7 +2880,7 @@ my %tests = (
                            procost,
                            prorows,
                            provariadic,
-                           protransform,
+                           prosupport,
                            prokind,
                            prosecdef,
                            proleakproof,
@@ -2912,7 +2912,7 @@ my %tests = (
         \QGRANT SELECT(procost) ON TABLE pg_catalog.pg_proc TO PUBLIC;\E\n.*
         \QGRANT SELECT(prorows) ON TABLE pg_catalog.pg_proc TO PUBLIC;\E\n.*
         \QGRANT SELECT(provariadic) ON TABLE pg_catalog.pg_proc TO PUBLIC;\E\n.*
-        \QGRANT SELECT(protransform) ON TABLE pg_catalog.pg_proc TO PUBLIC;\E\n.*
+        \QGRANT SELECT(prosupport) ON TABLE pg_catalog.pg_proc TO PUBLIC;\E\n.*
         \QGRANT SELECT(prokind) ON TABLE pg_catalog.pg_proc TO PUBLIC;\E\n.*
         \QGRANT SELECT(prosecdef) ON TABLE pg_catalog.pg_proc TO PUBLIC;\E\n.*
         \QGRANT SELECT(proleakproof) ON TABLE pg_catalog.pg_proc TO PUBLIC;\E\n.*
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3ecc2e1..e5cb5bb 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -1326,11 +1326,11 @@
 { oid => '668', descr => 'adjust char() to typmod length',
   proname => 'bpchar', prorettype => 'bpchar',
   proargtypes => 'bpchar int4 bool', prosrc => 'bpchar' },
-{ oid => '3097', descr => 'transform a varchar length coercion',
-  proname => 'varchar_transform', prorettype => 'internal',
-  proargtypes => 'internal', prosrc => 'varchar_transform' },
+{ oid => '3097', descr => 'planner support for varchar length coercion',
+  proname => 'varchar_support', prorettype => 'internal',
+  proargtypes => 'internal', prosrc => 'varchar_support' },
 { oid => '669', descr => 'adjust varchar() to typmod length',
-  proname => 'varchar', protransform => 'varchar_transform',
+  proname => 'varchar', prosupport => 'varchar_support',
   prorettype => 'varchar', proargtypes => 'varchar int4 bool',
   prosrc => 'varchar' },

@@ -1954,13 +1954,9 @@

 # OIDS 1000 - 1999

-{ oid => '3994', descr => 'transform a time zone adjustment',
-  proname => 'timestamp_izone_transform', prorettype => 'internal',
-  proargtypes => 'internal', prosrc => 'timestamp_izone_transform' },
 { oid => '1026', descr => 'adjust timestamp to new time zone',
-  proname => 'timezone', protransform => 'timestamp_izone_transform',
-  prorettype => 'timestamp', proargtypes => 'interval timestamptz',
-  prosrc => 'timestamptz_izone' },
+  proname => 'timezone', prorettype => 'timestamp',
+  proargtypes => 'interval timestamptz', prosrc => 'timestamptz_izone' },

 { oid => '1031', descr => 'I/O',
   proname => 'aclitemin', provolatile => 's', prorettype => 'aclitem',
@@ -2190,13 +2186,9 @@
 { oid => '1158', descr => 'convert UNIX epoch to timestamptz',
   proname => 'to_timestamp', prorettype => 'timestamptz',
   proargtypes => 'float8', prosrc => 'float8_timestamptz' },
-{ oid => '3995', descr => 'transform a time zone adjustment',
-  proname => 'timestamp_zone_transform', prorettype => 'internal',
-  proargtypes => 'internal', prosrc => 'timestamp_zone_transform' },
 { oid => '1159', descr => 'adjust timestamp to new time zone',
-  proname => 'timezone', protransform => 'timestamp_zone_transform',
-  prorettype => 'timestamp', proargtypes => 'text timestamptz',
-  prosrc => 'timestamptz_zone' },
+  proname => 'timezone', prorettype => 'timestamp',
+  proargtypes => 'text timestamptz', prosrc => 'timestamptz_zone' },

 { oid => '1160', descr => 'I/O',
   proname => 'interval_in', provolatile => 's', prorettype => 'interval',
@@ -2301,11 +2293,11 @@

 # OIDS 1200 - 1299

-{ oid => '3918', descr => 'transform an interval length coercion',
-  proname => 'interval_transform', prorettype => 'internal',
-  proargtypes => 'internal', prosrc => 'interval_transform' },
+{ oid => '3918', descr => 'planner support for interval length coercion',
+  proname => 'interval_support', prorettype => 'internal',
+  proargtypes => 'internal', prosrc => 'interval_support' },
 { oid => '1200', descr => 'adjust interval precision',
-  proname => 'interval', protransform => 'interval_transform',
+  proname => 'interval', prosupport => 'interval_support',
   prorettype => 'interval', proargtypes => 'interval int4',
   prosrc => 'interval_scale' },

@@ -3713,13 +3705,12 @@
 { oid => '1685', descr => 'adjust bit() to typmod length',
   proname => 'bit', prorettype => 'bit', proargtypes => 'bit int4 bool',
   prosrc => 'bit' },
-{ oid => '3158', descr => 'transform a varbit length coercion',
-  proname => 'varbit_transform', prorettype => 'internal',
-  proargtypes => 'internal', prosrc => 'varbit_transform' },
+{ oid => '3158', descr => 'planner support for varbit length coercion',
+  proname => 'varbit_support', prorettype => 'internal',
+  proargtypes => 'internal', prosrc => 'varbit_support' },
 { oid => '1687', descr => 'adjust varbit() to typmod length',
-  proname => 'varbit', protransform => 'varbit_transform',
-  prorettype => 'varbit', proargtypes => 'varbit int4 bool',
-  prosrc => 'varbit' },
+  proname => 'varbit', prosupport => 'varbit_support', prorettype => 'varbit',
+  proargtypes => 'varbit int4 bool', prosrc => 'varbit' },

 { oid => '1698', descr => 'position of sub-bitstring',
   proname => 'position', prorettype => 'int4', proargtypes => 'bit bit',
@@ -4081,11 +4072,11 @@
 { oid => '2918', descr => 'I/O typmod',
   proname => 'numerictypmodout', prorettype => 'cstring', proargtypes => 'int4',
   prosrc => 'numerictypmodout' },
-{ oid => '3157', descr => 'transform a numeric length coercion',
-  proname => 'numeric_transform', prorettype => 'internal',
-  proargtypes => 'internal', prosrc => 'numeric_transform' },
+{ oid => '3157', descr => 'planner support for numeric length coercion',
+  proname => 'numeric_support', prorettype => 'internal',
+  proargtypes => 'internal', prosrc => 'numeric_support' },
 { oid => '1703', descr => 'adjust numeric to typmod precision/scale',
-  proname => 'numeric', protransform => 'numeric_transform',
+  proname => 'numeric', prosupport => 'numeric_support',
   prorettype => 'numeric', proargtypes => 'numeric int4', prosrc => 'numeric' },
 { oid => '1704',
   proname => 'numeric_abs', prorettype => 'numeric', proargtypes => 'numeric',
@@ -5448,15 +5439,15 @@
   proname => 'bytea_sortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'bytea_sortsupport' },

-{ oid => '3917', descr => 'transform a timestamp length coercion',
-  proname => 'timestamp_transform', prorettype => 'internal',
-  proargtypes => 'internal', prosrc => 'timestamp_transform' },
-{ oid => '3944', descr => 'transform a time length coercion',
-  proname => 'time_transform', prorettype => 'internal',
-  proargtypes => 'internal', prosrc => 'time_transform' },
+{ oid => '3917', descr => 'planner support for timestamp length coercion',
+  proname => 'timestamp_support', prorettype => 'internal',
+  proargtypes => 'internal', prosrc => 'timestamp_support' },
+{ oid => '3944', descr => 'planner support for time length coercion',
+  proname => 'time_support', prorettype => 'internal',
+  proargtypes => 'internal', prosrc => 'time_support' },

 { oid => '1961', descr => 'adjust timestamp precision',
-  proname => 'timestamp', protransform => 'timestamp_transform',
+  proname => 'timestamp', prosupport => 'timestamp_support',
   prorettype => 'timestamp', proargtypes => 'timestamp int4',
   prosrc => 'timestamp_scale' },

@@ -5468,14 +5459,14 @@
   prosrc => 'oidsmaller' },

 { oid => '1967', descr => 'adjust timestamptz precision',
-  proname => 'timestamptz', protransform => 'timestamp_transform',
+  proname => 'timestamptz', prosupport => 'timestamp_support',
   prorettype => 'timestamptz', proargtypes => 'timestamptz int4',
   prosrc => 'timestamptz_scale' },
 { oid => '1968', descr => 'adjust time precision',
-  proname => 'time', protransform => 'time_transform', prorettype => 'time',
+  proname => 'time', prosupport => 'time_support', prorettype => 'time',
   proargtypes => 'time int4', prosrc => 'time_scale' },
 { oid => '1969', descr => 'adjust time with time zone precision',
-  proname => 'timetz', protransform => 'time_transform', prorettype => 'timetz',
+  proname => 'timetz', prosupport => 'time_support', prorettype => 'timetz',
   proargtypes => 'timetz int4', prosrc => 'timetz_scale' },

 { oid => '2003',
@@ -5662,13 +5653,11 @@
   prosrc => 'select pg_catalog.age(cast(current_date as timestamp without time zone), $1)' },

 { oid => '2069', descr => 'adjust timestamp to new time zone',
-  proname => 'timezone', protransform => 'timestamp_zone_transform',
-  prorettype => 'timestamptz', proargtypes => 'text timestamp',
-  prosrc => 'timestamp_zone' },
+  proname => 'timezone', prorettype => 'timestamptz',
+  proargtypes => 'text timestamp', prosrc => 'timestamp_zone' },
 { oid => '2070', descr => 'adjust timestamp to new time zone',
-  proname => 'timezone', protransform => 'timestamp_izone_transform',
-  prorettype => 'timestamptz', proargtypes => 'interval timestamp',
-  prosrc => 'timestamp_izone' },
+  proname => 'timezone', prorettype => 'timestamptz',
+  proargtypes => 'interval timestamp', prosrc => 'timestamp_izone' },
 { oid => '2071',
   proname => 'date_pl_interval', prorettype => 'timestamp',
   proargtypes => 'date interval', prosrc => 'date_pl_interval' },
diff --git a/src/include/catalog/pg_proc.h b/src/include/catalog/pg_proc.h
index c2bb951..b433769 100644
--- a/src/include/catalog/pg_proc.h
+++ b/src/include/catalog/pg_proc.h
@@ -53,8 +53,8 @@ CATALOG(pg_proc,1255,ProcedureRelationId) BKI_BOOTSTRAP BKI_ROWTYPE_OID(81,Proce
     /* element type of variadic array, or 0 */
     Oid            provariadic BKI_DEFAULT(0) BKI_LOOKUP(pg_type);

-    /* transforms calls to it during planning */
-    regproc        protransform BKI_DEFAULT(0) BKI_LOOKUP(pg_proc);
+    /* planner support function for this function, or 0 if none */
+    regproc        prosupport BKI_DEFAULT(0) BKI_LOOKUP(pg_proc);

     /* see PROKIND_ categories below */
     char        prokind BKI_DEFAULT(f);
diff --git a/src/include/nodes/nodes.h b/src/include/nodes/nodes.h
index 10dac60..e029b40 100644
--- a/src/include/nodes/nodes.h
+++ b/src/include/nodes/nodes.h
@@ -505,7 +505,8 @@ typedef enum NodeTag
     T_IndexAmRoutine,            /* in access/amapi.h */
     T_TsmRoutine,                /* in access/tsmapi.h */
     T_ForeignKeyCacheInfo,        /* in utils/rel.h */
-    T_CallContext                /* in nodes/parsenodes.h */
+    T_CallContext,                /* in nodes/parsenodes.h */
+    T_SupportRequestSimplify    /* in nodes/supportnodes.h */
 } NodeTag;

 /*
diff --git a/src/include/nodes/supportnodes.h b/src/include/nodes/supportnodes.h
new file mode 100644
index 0000000..1f7d02b
--- /dev/null
+++ b/src/include/nodes/supportnodes.h
@@ -0,0 +1,70 @@
+/*-------------------------------------------------------------------------
+ *
+ * supportnodes.h
+ *      Definitions for planner support functions.
+ *
+ * This file defines the API for "planner support functions", which
+ * are SQL functions (normally written in C) that can be attached to
+ * another "target" function to give the system additional knowledge
+ * about the target function.  All the current capabilities have to do
+ * with planning queries that use the target function, though it is
+ * possible that future extensions will add functionality to be invoked
+ * by the parser or executor.
+ *
+ * A support function must have the SQL signature
+ *        supportfn(internal) returns internal
+ * The argument is a pointer to one of the Node types defined in this file.
+ * The result is usually also a Node pointer, though its type depends on
+ * which capability is being invoked.  In all cases, a NULL pointer result
+ * (that's PG_RETURN_POINTER(NULL), not PG_RETURN_NULL()) indicates that
+ * the support function cannot do anything useful for the given request.
+ * Support functions must return a NULL pointer, not fail, if they do not
+ * recognize the request node type or cannot handle the given case; this
+ * allows for future extensions of the set of request cases.
+ *
+ *
+ * Portions Copyright (c) 1996-2019, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/nodes/supportnodes.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef SUPPORTNODES_H
+#define SUPPORTNODES_H
+
+#include "nodes/primnodes.h"
+
+struct PlannerInfo;                /* avoid including relation.h here */
+
+
+/*
+ * The Simplify request allows the support function to perform plan-time
+ * simplification of a call to its target function.  For example, a varchar
+ * length coercion that does not decrease the allowed length of its argument
+ * could be replaced by a RelabelType node, or "x + 0" could be replaced by
+ * "x".  This is invoked during the planner's constant-folding pass, so the
+ * function's arguments can be presumed already simplified.
+ *
+ * The planner's PlannerInfo "root" is typically not needed, but can be
+ * consulted if it's necessary to obtain info about Vars present in
+ * the given node tree.  Beware that root could be NULL in some usages.
+ *
+ * "fcall" will be a FuncExpr invoking the support function's target
+ * function.  (This is true even if the original parsetree node was an
+ * operator call; a FuncExpr is synthesized for this purpose.)
+ *
+ * The result should be a semantically-equivalent transformed node tree,
+ * or NULL if no simplification could be performed.  Do *not* return or
+ * modify *fcall, as it isn't really a separately allocated Node.  But
+ * it's okay to use fcall->args, or parts of it, in the result tree.
+ */
+typedef struct SupportRequestSimplify
+{
+    NodeTag        type;
+
+    struct PlannerInfo *root;    /* Planner's infrastructure */
+    FuncExpr   *fcall;            /* Function call to be simplified */
+} SupportRequestSimplify;
+
+#endif                            /* SUPPORTNODES_H */
diff --git a/src/include/utils/datetime.h b/src/include/utils/datetime.h
index f5ec9bb..87f819e 100644
--- a/src/include/utils/datetime.h
+++ b/src/include/utils/datetime.h
@@ -330,7 +330,7 @@ extern int    DecodeUnits(int field, char *lowtoken, int *val);

 extern int    j2day(int jd);

-extern Node *TemporalTransform(int32 max_precis, Node *node);
+extern Node *TemporalSimplify(int32 max_precis, Node *node);

 extern bool CheckDateTokenTables(void);

diff --git a/src/test/modules/test_ddl_deparse/expected/create_transform.out
b/src/test/modules/test_ddl_deparse/expected/create_transform.out
index 0d1cc36..da7fea2 100644
--- a/src/test/modules/test_ddl_deparse/expected/create_transform.out
+++ b/src/test/modules/test_ddl_deparse/expected/create_transform.out
@@ -7,7 +7,7 @@
 -- internal and as return argument the datatype of the transform done.
 -- pl/plpgsql does not authorize the use of internal as data type.
 CREATE TRANSFORM FOR int LANGUAGE SQL (
-    FROM SQL WITH FUNCTION varchar_transform(internal),
+    FROM SQL WITH FUNCTION varchar_support(internal),
     TO SQL WITH FUNCTION int4recv(internal));
 NOTICE:  DDL test: type simple, tag CREATE TRANSFORM
 DROP TRANSFORM FOR int LANGUAGE SQL;
diff --git a/src/test/modules/test_ddl_deparse/sql/create_transform.sql
b/src/test/modules/test_ddl_deparse/sql/create_transform.sql
index 0968702..132fc5a 100644
--- a/src/test/modules/test_ddl_deparse/sql/create_transform.sql
+++ b/src/test/modules/test_ddl_deparse/sql/create_transform.sql
@@ -8,7 +8,7 @@
 -- internal and as return argument the datatype of the transform done.
 -- pl/plpgsql does not authorize the use of internal as data type.
 CREATE TRANSFORM FOR int LANGUAGE SQL (
-    FROM SQL WITH FUNCTION varchar_transform(internal),
+    FROM SQL WITH FUNCTION varchar_support(internal),
     TO SQL WITH FUNCTION int4recv(internal));

 DROP TRANSFORM FOR int LANGUAGE SQL;
diff --git a/src/test/regress/expected/object_address.out b/src/test/regress/expected/object_address.out
index 4085e45..c89ec06 100644
--- a/src/test/regress/expected/object_address.out
+++ b/src/test/regress/expected/object_address.out
@@ -38,7 +38,7 @@ CREATE USER MAPPING FOR regress_addr_user SERVER "integer";
 ALTER DEFAULT PRIVILEGES FOR ROLE regress_addr_user IN SCHEMA public GRANT ALL ON TABLES TO regress_addr_user;
 ALTER DEFAULT PRIVILEGES FOR ROLE regress_addr_user REVOKE DELETE ON TABLES FROM regress_addr_user;
 CREATE TRANSFORM FOR int LANGUAGE SQL (
-    FROM SQL WITH FUNCTION varchar_transform(internal),
+    FROM SQL WITH FUNCTION varchar_support(internal),
     TO SQL WITH FUNCTION int4recv(internal));
 CREATE PUBLICATION addr_pub FOR TABLE addr_nsp.gentable;
 CREATE SUBSCRIPTION addr_sub CONNECTION '' PUBLICATION bar WITH (connect = false, slot_name = NONE);
diff --git a/src/test/regress/expected/oidjoins.out b/src/test/regress/expected/oidjoins.out
index ef268d3..4edc817 100644
--- a/src/test/regress/expected/oidjoins.out
+++ b/src/test/regress/expected/oidjoins.out
@@ -809,12 +809,12 @@ WHERE    provariadic != 0 AND
 ------+-------------
 (0 rows)

-SELECT    ctid, protransform
+SELECT    ctid, prosupport
 FROM    pg_catalog.pg_proc fk
-WHERE    protransform != 0 AND
-    NOT EXISTS(SELECT 1 FROM pg_catalog.pg_proc pk WHERE pk.oid = fk.protransform);
- ctid | protransform
-------+--------------
+WHERE    prosupport != 0 AND
+    NOT EXISTS(SELECT 1 FROM pg_catalog.pg_proc pk WHERE pk.oid = fk.prosupport);
+ ctid | prosupport
+------+------------
 (0 rows)

 SELECT    ctid, prorettype
diff --git a/src/test/regress/expected/opr_sanity.out b/src/test/regress/expected/opr_sanity.out
index 7328095..ce25ee0 100644
--- a/src/test/regress/expected/opr_sanity.out
+++ b/src/test/regress/expected/opr_sanity.out
@@ -453,10 +453,10 @@ WHERE proallargtypes IS NOT NULL AND
 -----+---------+-------------+----------------+-------------
 (0 rows)

--- Check for protransform functions with the wrong signature
+-- Check for prosupport functions with the wrong signature
 SELECT p1.oid, p1.proname, p2.oid, p2.proname
 FROM pg_proc AS p1, pg_proc AS p2
-WHERE p2.oid = p1.protransform AND
+WHERE p2.oid = p1.prosupport AND
     (p2.prorettype != 'internal'::regtype OR p2.proretset OR p2.pronargs != 1
      OR p2.proargtypes[0] != 'internal'::regtype);
  oid | proname | oid | proname
diff --git a/src/test/regress/sql/object_address.sql b/src/test/regress/sql/object_address.sql
index d7df322..fd79465 100644
--- a/src/test/regress/sql/object_address.sql
+++ b/src/test/regress/sql/object_address.sql
@@ -41,7 +41,7 @@ CREATE USER MAPPING FOR regress_addr_user SERVER "integer";
 ALTER DEFAULT PRIVILEGES FOR ROLE regress_addr_user IN SCHEMA public GRANT ALL ON TABLES TO regress_addr_user;
 ALTER DEFAULT PRIVILEGES FOR ROLE regress_addr_user REVOKE DELETE ON TABLES FROM regress_addr_user;
 CREATE TRANSFORM FOR int LANGUAGE SQL (
-    FROM SQL WITH FUNCTION varchar_transform(internal),
+    FROM SQL WITH FUNCTION varchar_support(internal),
     TO SQL WITH FUNCTION int4recv(internal));
 CREATE PUBLICATION addr_pub FOR TABLE addr_nsp.gentable;
 CREATE SUBSCRIPTION addr_sub CONNECTION '' PUBLICATION bar WITH (connect = false, slot_name = NONE);
diff --git a/src/test/regress/sql/oidjoins.sql b/src/test/regress/sql/oidjoins.sql
index c8291d3..dbe4a58 100644
--- a/src/test/regress/sql/oidjoins.sql
+++ b/src/test/regress/sql/oidjoins.sql
@@ -405,10 +405,10 @@ SELECT    ctid, provariadic
 FROM    pg_catalog.pg_proc fk
 WHERE    provariadic != 0 AND
     NOT EXISTS(SELECT 1 FROM pg_catalog.pg_type pk WHERE pk.oid = fk.provariadic);
-SELECT    ctid, protransform
+SELECT    ctid, prosupport
 FROM    pg_catalog.pg_proc fk
-WHERE    protransform != 0 AND
-    NOT EXISTS(SELECT 1 FROM pg_catalog.pg_proc pk WHERE pk.oid = fk.protransform);
+WHERE    prosupport != 0 AND
+    NOT EXISTS(SELECT 1 FROM pg_catalog.pg_proc pk WHERE pk.oid = fk.prosupport);
 SELECT    ctid, prorettype
 FROM    pg_catalog.pg_proc fk
 WHERE    prorettype != 0 AND
diff --git a/src/test/regress/sql/opr_sanity.sql b/src/test/regress/sql/opr_sanity.sql
index 8544cbe..e2014fc 100644
--- a/src/test/regress/sql/opr_sanity.sql
+++ b/src/test/regress/sql/opr_sanity.sql
@@ -353,10 +353,10 @@ WHERE proallargtypes IS NOT NULL AND
         FROM generate_series(1, array_length(proallargtypes, 1)) g(i)
         WHERE proargmodes IS NULL OR proargmodes[i] IN ('i', 'b', 'v'));

--- Check for protransform functions with the wrong signature
+-- Check for prosupport functions with the wrong signature
 SELECT p1.oid, p1.proname, p2.oid, p2.proname
 FROM pg_proc AS p1, pg_proc AS p2
-WHERE p2.oid = p1.protransform AND
+WHERE p2.oid = p1.prosupport AND
     (p2.prorettype != 'internal'::regtype OR p2.proretset OR p2.pronargs != 1
      OR p2.proargtypes[0] != 'internal'::regtype);

diff --git a/src/tools/findoidjoins/README b/src/tools/findoidjoins/README
index 305454a..e5fc310 100644
--- a/src/tools/findoidjoins/README
+++ b/src/tools/findoidjoins/README
@@ -161,7 +161,7 @@ Join pg_catalog.pg_proc.pronamespace => pg_catalog.pg_namespace.oid
 Join pg_catalog.pg_proc.proowner => pg_catalog.pg_authid.oid
 Join pg_catalog.pg_proc.prolang => pg_catalog.pg_language.oid
 Join pg_catalog.pg_proc.provariadic => pg_catalog.pg_type.oid
-Join pg_catalog.pg_proc.protransform => pg_catalog.pg_proc.oid
+Join pg_catalog.pg_proc.prosupport => pg_catalog.pg_proc.oid
 Join pg_catalog.pg_proc.prorettype => pg_catalog.pg_type.oid
 Join pg_catalog.pg_range.rngtypid => pg_catalog.pg_type.oid
 Join pg_catalog.pg_range.rngsubtype => pg_catalog.pg_type.oid
diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index af4d062..6dd0700 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -5146,11 +5146,11 @@ SCRAM-SHA-256$<replaceable><iteration count></replaceable>:<replaceable>&l
      </row>

      <row>
-      <entry><structfield>protransform</structfield></entry>
+      <entry><structfield>prosupport</structfield></entry>
       <entry><type>regproc</type></entry>
       <entry><literal><link linkend="catalog-pg-proc"><structname>pg_proc</structname></link>.oid</literal></entry>
-      <entry>Calls to this function can be simplified by this other function
-       (see <xref linkend="xfunc-transform-functions"/>)</entry>
+      <entry>Optional planner support function for this function
+       (see <xref linkend="xfunc-optimization"/>)</entry>
      </row>

      <row>
diff --git a/doc/src/sgml/xfunc.sgml b/doc/src/sgml/xfunc.sgml
index e18272c..d70aa6e 100644
--- a/doc/src/sgml/xfunc.sgml
+++ b/doc/src/sgml/xfunc.sgml
@@ -3241,40 +3241,6 @@ CREATE FUNCTION make_array(anyelement) RETURNS anyarray
     </para>
    </sect2>

-   <sect2 id="xfunc-transform-functions">
-    <title>Transform Functions</title>
-
-    <para>
-     Some function calls can be simplified during planning based on
-     properties specific to the function.  For example,
-     <literal>int4mul(n, 1)</literal> could be simplified to just <literal>n</literal>.
-     To define such function-specific optimizations, write a
-     <firstterm>transform function</firstterm> and place its OID in the
-     <structfield>protransform</structfield> field of the primary function's
-     <structname>pg_proc</structname> entry.  The transform function must have the SQL
-     signature <literal>protransform(internal) RETURNS internal</literal>.  The
-     argument, actually <type>FuncExpr *</type>, is a dummy node representing a
-     call to the primary function.  If the transform function's study of the
-     expression tree proves that a simplified expression tree can substitute
-     for all possible concrete calls represented thereby, build and return
-     that simplified expression.  Otherwise, return a <literal>NULL</literal>
-     pointer (<emphasis>not</emphasis> a SQL null).
-    </para>
-
-    <para>
-     We make no guarantee that <productname>PostgreSQL</productname> will never call the
-     primary function in cases that the transform function could simplify.
-     Ensure rigorous equivalence between the simplified expression and an
-     actual call to the primary function.
-    </para>
-
-    <para>
-     Currently, this facility is not exposed to users at the SQL level
-     because of security concerns, so it is only practical to use for
-     optimizing built-in functions.
-    </para>
-   </sect2>
-
    <sect2>
     <title>Shared Memory and LWLocks</title>

@@ -3388,3 +3354,89 @@ if (!ptr)
    </sect2>

   </sect1>
+
+  <sect1 id="xfunc-optimization">
+   <title>Function Optimization Information</title>
+
+  <indexterm zone="xfunc-optimization">
+   <primary>optimization information</primary>
+   <secondary>for functions</secondary>
+  </indexterm>
+
+   <para>
+    By default, a function is just a <quote>black box</quote> that the
+    database system knows very little about the behavior of.  However,
+    that means that queries using the function may be executed much less
+    efficiently than they could be.  It is possible to supply additional
+    knowledge that helps the planner optimize function calls.
+   </para>
+
+   <para>
+    Some basic facts can be supplied by declarative annotations provided in
+    the <xref linkend="sql-createfunction"/> command.  Most important of
+    these is the function's <link linkend="xfunc-volatility">volatility
+    category</link> (<literal>IMMUTABLE</literal>, <literal>STABLE</literal>,
+    or <literal>VOLATILE</literal>); one should always be careful to
+    specify this correctly when defining a function.
+    The parallel safety property (<literal>PARALLEL
+    UNSAFE</literal>, <literal>PARALLEL RESTRICTED</literal>, or
+    <literal>PARALLEL SAFE</literal>) must also be specified if you hope
+    to use the function in parallelized queries.
+    It can also be useful to specify the function's estimated execution
+    cost, and/or the number of rows a set-returning function is estimated
+    to return.  However, the declarative way of specifying those two
+    facts only allows specifying a constant value, which is often
+    inadequate.
+   </para>
+
+   <para>
+    It is also possible to attach a <firstterm>planner support
+    function</firstterm> to a SQL-callable function (called
+    its <firstterm>target function</firstterm>), and thereby provide
+    knowledge about the target function that is too complex to be
+    represented declaratively.  Planner support functions have to be
+    written in C (although their target functions might not be), so this is
+    an advanced feature that relatively few people will use.
+   </para>
+
+   <para>
+    A planner support function must have the SQL signature
+<programlisting>
+supportfn(internal) returns internal
+</programlisting>
+    It is attached to its target function by specifying
+    the <literal>SUPPORT</literal> clause when creating the target function.
+   </para>
+
+   <para>
+    The details of the API for planner support functions can be found in
+    file <filename>src/include/nodes/supportnodes.h</filename> in the
+    <productname>PostgreSQL</productname> source code.  Here we provide
+    just an overview of what planner support functions can do.
+    The set of possible requests to a support function is extensible,
+    so more things might be possible in future versions.
+   </para>
+
+   <para>
+    Some function calls can be simplified during planning based on
+    properties specific to the function.  For example,
+    <literal>int4mul(n, 1)</literal> could be simplified to
+    just <literal>n</literal>.  This type of transformation can be
+    performed by a planner support function, by having it implement
+    the <literal>SupportRequestSimplify</literal> request type.
+    The support function will be called for each instance of its target
+    function found in a query parse tree.  If it finds that the particular
+    call can be simplified into some other form, it can build and return a
+    parse tree representing that expression.  This will automatically work
+    for operators based on the function, too — in the example just
+    given, <literal>n * 1</literal> would also be simplified to
+    <literal>n</literal>.
+    (But note that this is just an example; this particular
+    optimization is not actually performed by
+    standard <productname>PostgreSQL</productname>.)
+    We make no guarantee that <productname>PostgreSQL</productname> will
+    never call the target function in cases that the support function could
+    simplify.  Ensure rigorous equivalence between the simplified
+    expression and an actual execution of the target function.
+   </para>
+  </sect1>
diff --git a/doc/src/sgml/xoper.sgml b/doc/src/sgml/xoper.sgml
index 2f5560a..260e43c 100644
--- a/doc/src/sgml/xoper.sgml
+++ b/doc/src/sgml/xoper.sgml
@@ -78,6 +78,11 @@ SELECT (a + b) AS c FROM test_complex;
   <sect1 id="xoper-optimization">
    <title>Operator Optimization Information</title>

+  <indexterm zone="xoper-optimization">
+   <primary>optimization information</primary>
+   <secondary>for operators</secondary>
+  </indexterm>
+
    <para>
     A <productname>PostgreSQL</productname> operator definition can include
     several optional clauses that tell the system useful things about how
@@ -97,6 +102,13 @@ SELECT (a + b) AS c FROM test_complex;
     the ones that release &version; understands.
    </para>

+   <para>
+    It is also possible to attach a planner support function to the function
+    that underlies an operator, providing another way of telling the system
+    about the behavior of the operator.
+    See <xref linkend="xfunc-optimization"/> for more information.
+   </para>
+
    <sect2>
     <title><literal>COMMUTATOR</literal></title>

diff --git a/src/backend/catalog/pg_proc.c b/src/backend/catalog/pg_proc.c
index db78061..3a86f1e 100644
--- a/src/backend/catalog/pg_proc.c
+++ b/src/backend/catalog/pg_proc.c
@@ -319,7 +319,7 @@ ProcedureCreate(const char *procedureName,
     values[Anum_pg_proc_procost - 1] = Float4GetDatum(procost);
     values[Anum_pg_proc_prorows - 1] = Float4GetDatum(prorows);
     values[Anum_pg_proc_provariadic - 1] = ObjectIdGetDatum(variadicType);
-    values[Anum_pg_proc_protransform - 1] = ObjectIdGetDatum(InvalidOid);
+    values[Anum_pg_proc_prosupport - 1] = ObjectIdGetDatum(InvalidOid);
     values[Anum_pg_proc_prokind - 1] = CharGetDatum(prokind);
     values[Anum_pg_proc_prosecdef - 1] = BoolGetDatum(security_definer);
     values[Anum_pg_proc_proleakproof - 1] = BoolGetDatum(isLeakProof);
diff --git a/src/backend/optimizer/util/clauses.c b/src/backend/optimizer/util/clauses.c
index f0ef102..061a855 100644
--- a/src/backend/optimizer/util/clauses.c
+++ b/src/backend/optimizer/util/clauses.c
@@ -32,6 +32,7 @@
 #include "miscadmin.h"
 #include "nodes/makefuncs.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/supportnodes.h"
 #include "optimizer/clauses.h"
 #include "optimizer/cost.h"
 #include "optimizer/planmain.h"
@@ -4269,13 +4270,16 @@ simplify_function(Oid funcid, Oid result_type, int32 result_typmod,
                                 args, funcvariadic,
                                 func_tuple, context);

-    if (!newexpr && allow_non_const && OidIsValid(func_form->protransform))
+    if (!newexpr && allow_non_const && OidIsValid(func_form->prosupport))
     {
         /*
-         * Build a dummy FuncExpr node containing the simplified arg list.  We
-         * use this approach to present a uniform interface to the transform
-         * function regardless of how the function is actually being invoked.
+         * Build a SupportRequestSimplify node to pass to the support
+         * function, pointing to a dummy FuncExpr node containing the
+         * simplified arg list.  We use this approach to present a uniform
+         * interface to the support function regardless of how the target
+         * function is actually being invoked.
          */
+        SupportRequestSimplify req;
         FuncExpr    fexpr;

         fexpr.xpr.type = T_FuncExpr;
@@ -4289,9 +4293,16 @@ simplify_function(Oid funcid, Oid result_type, int32 result_typmod,
         fexpr.args = args;
         fexpr.location = -1;

+        req.type = T_SupportRequestSimplify;
+        req.root = context->root;
+        req.fcall = &fexpr;
+
         newexpr = (Expr *)
-            DatumGetPointer(OidFunctionCall1(func_form->protransform,
-                                             PointerGetDatum(&fexpr)));
+            DatumGetPointer(OidFunctionCall1(func_form->prosupport,
+                                             PointerGetDatum(&req)));
+
+        /* catch a possible API misunderstanding */
+        Assert(newexpr != (Expr *) &fexpr);
     }

     if (!newexpr && allow_non_const)
diff --git a/src/backend/utils/adt/date.c b/src/backend/utils/adt/date.c
index 3810e4a..cf5a1c6 100644
--- a/src/backend/utils/adt/date.c
+++ b/src/backend/utils/adt/date.c
@@ -24,6 +24,7 @@
 #include "access/xact.h"
 #include "libpq/pqformat.h"
 #include "miscadmin.h"
+#include "nodes/supportnodes.h"
 #include "parser/scansup.h"
 #include "utils/array.h"
 #include "utils/builtins.h"
@@ -1341,15 +1342,25 @@ make_time(PG_FUNCTION_ARGS)
 }


-/* time_transform()
- * Flatten calls to time_scale() and timetz_scale() that solely represent
- * increases in allowed precision.
+/* time_support()
+ *
+ * Planner support function for the time_scale() and timetz_scale()
+ * length coercion functions (we need not distinguish them here).
  */
 Datum
-time_transform(PG_FUNCTION_ARGS)
+time_support(PG_FUNCTION_ARGS)
 {
-    PG_RETURN_POINTER(TemporalTransform(MAX_TIME_PRECISION,
-                                        (Node *) PG_GETARG_POINTER(0)));
+    Node       *rawreq = (Node *) PG_GETARG_POINTER(0);
+    Node       *ret = NULL;
+
+    if (IsA(rawreq, SupportRequestSimplify))
+    {
+        SupportRequestSimplify *req = (SupportRequestSimplify *) rawreq;
+
+        ret = TemporalSimplify(MAX_TIME_PRECISION, (Node *) req->fcall);
+    }
+
+    PG_RETURN_POINTER(ret);
 }

 /* time_scale()
diff --git a/src/backend/utils/adt/datetime.c b/src/backend/utils/adt/datetime.c
index 61dbd05..0068e71 100644
--- a/src/backend/utils/adt/datetime.c
+++ b/src/backend/utils/adt/datetime.c
@@ -4462,16 +4462,23 @@ CheckDateTokenTables(void)
 }

 /*
- * Common code for temporal protransform functions.  Types time, timetz,
- * timestamp and timestamptz each have a range of allowed precisions.  An
- * unspecified precision is rigorously equivalent to the highest specifiable
- * precision.
+ * Common code for temporal prosupport functions: simplify, if possible,
+ * a call to a temporal type's length-coercion function.
+ *
+ * Types time, timetz, timestamp and timestamptz each have a range of allowed
+ * precisions.  An unspecified precision is rigorously equivalent to the
+ * highest specifiable precision.  We can replace the function call with a
+ * no-op RelabelType if it is coercing to the same or higher precision as the
+ * input is known to have.
+ *
+ * The input Node is always a FuncExpr, but to reduce the #include footprint
+ * of datetime.h, we declare it as Node *.
  *
  * Note: timestamp_scale throws an error when the typmod is out of range, but
  * we can't get there from a cast: our typmodin will have caught it already.
  */
 Node *
-TemporalTransform(int32 max_precis, Node *node)
+TemporalSimplify(int32 max_precis, Node *node)
 {
     FuncExpr   *expr = castNode(FuncExpr, node);
     Node       *ret = NULL;
diff --git a/src/backend/utils/adt/numeric.c b/src/backend/utils/adt/numeric.c
index 45cd1a0..1c9deeb 100644
--- a/src/backend/utils/adt/numeric.c
+++ b/src/backend/utils/adt/numeric.c
@@ -34,6 +34,7 @@
 #include "libpq/pqformat.h"
 #include "miscadmin.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/supportnodes.h"
 #include "utils/array.h"
 #include "utils/builtins.h"
 #include "utils/float.h"
@@ -890,45 +891,53 @@ numeric_send(PG_FUNCTION_ARGS)


 /*
- * numeric_transform() -
+ * numeric_support()
  *
- * Flatten calls to numeric's length coercion function that solely represent
- * increases in allowable precision.  Scale changes mutate every datum, so
- * they are unoptimizable.  Some values, e.g. 1E-1001, can only fit into an
- * unconstrained numeric, so a change from an unconstrained numeric to any
- * constrained numeric is also unoptimizable.
+ * Planner support function for the numeric() length coercion function.
+ *
+ * Flatten calls that solely represent increases in allowable precision.
+ * Scale changes mutate every datum, so they are unoptimizable.  Some values,
+ * e.g. 1E-1001, can only fit into an unconstrained numeric, so a change from
+ * an unconstrained numeric to any constrained numeric is also unoptimizable.
  */
 Datum
-numeric_transform(PG_FUNCTION_ARGS)
+numeric_support(PG_FUNCTION_ARGS)
 {
-    FuncExpr   *expr = castNode(FuncExpr, PG_GETARG_POINTER(0));
+    Node       *rawreq = (Node *) PG_GETARG_POINTER(0);
     Node       *ret = NULL;
-    Node       *typmod;

-    Assert(list_length(expr->args) >= 2);
+    if (IsA(rawreq, SupportRequestSimplify))
+    {
+        SupportRequestSimplify *req = (SupportRequestSimplify *) rawreq;
+        FuncExpr   *expr = req->fcall;
+        Node       *typmod;

-    typmod = (Node *) lsecond(expr->args);
+        Assert(list_length(expr->args) >= 2);

-    if (IsA(typmod, Const) &&!((Const *) typmod)->constisnull)
-    {
-        Node       *source = (Node *) linitial(expr->args);
-        int32        old_typmod = exprTypmod(source);
-        int32        new_typmod = DatumGetInt32(((Const *) typmod)->constvalue);
-        int32        old_scale = (old_typmod - VARHDRSZ) & 0xffff;
-        int32        new_scale = (new_typmod - VARHDRSZ) & 0xffff;
-        int32        old_precision = (old_typmod - VARHDRSZ) >> 16 & 0xffff;
-        int32        new_precision = (new_typmod - VARHDRSZ) >> 16 & 0xffff;
+        typmod = (Node *) lsecond(expr->args);

-        /*
-         * If new_typmod < VARHDRSZ, the destination is unconstrained; that's
-         * always OK.  If old_typmod >= VARHDRSZ, the source is constrained,
-         * and we're OK if the scale is unchanged and the precision is not
-         * decreasing.  See further notes in function header comment.
-         */
-        if (new_typmod < (int32) VARHDRSZ ||
-            (old_typmod >= (int32) VARHDRSZ &&
-             new_scale == old_scale && new_precision >= old_precision))
-            ret = relabel_to_typmod(source, new_typmod);
+        if (IsA(typmod, Const) &&!((Const *) typmod)->constisnull)
+        {
+            Node       *source = (Node *) linitial(expr->args);
+            int32        old_typmod = exprTypmod(source);
+            int32        new_typmod = DatumGetInt32(((Const *) typmod)->constvalue);
+            int32        old_scale = (old_typmod - VARHDRSZ) & 0xffff;
+            int32        new_scale = (new_typmod - VARHDRSZ) & 0xffff;
+            int32        old_precision = (old_typmod - VARHDRSZ) >> 16 & 0xffff;
+            int32        new_precision = (new_typmod - VARHDRSZ) >> 16 & 0xffff;
+
+            /*
+             * If new_typmod < VARHDRSZ, the destination is unconstrained;
+             * that's always OK.  If old_typmod >= VARHDRSZ, the source is
+             * constrained, and we're OK if the scale is unchanged and the
+             * precision is not decreasing.  See further notes in function
+             * header comment.
+             */
+            if (new_typmod < (int32) VARHDRSZ ||
+                (old_typmod >= (int32) VARHDRSZ &&
+                 new_scale == old_scale && new_precision >= old_precision))
+                ret = relabel_to_typmod(source, new_typmod);
+        }
     }

     PG_RETURN_POINTER(ret);
diff --git a/src/backend/utils/adt/timestamp.c b/src/backend/utils/adt/timestamp.c
index 7befb6a..e0ef2f7 100644
--- a/src/backend/utils/adt/timestamp.c
+++ b/src/backend/utils/adt/timestamp.c
@@ -29,6 +29,7 @@
 #include "miscadmin.h"
 #include "nodes/makefuncs.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/supportnodes.h"
 #include "parser/scansup.h"
 #include "utils/array.h"
 #include "utils/builtins.h"
@@ -297,15 +298,26 @@ timestamptypmodout(PG_FUNCTION_ARGS)
 }


-/* timestamp_transform()
- * Flatten calls to timestamp_scale() and timestamptz_scale() that solely
- * represent increases in allowed precision.
+/*
+ * timestamp_support()
+ *
+ * Planner support function for the timestamp_scale() and timestamptz_scale()
+ * length coercion functions (we need not distinguish them here).
  */
 Datum
-timestamp_transform(PG_FUNCTION_ARGS)
+timestamp_support(PG_FUNCTION_ARGS)
 {
-    PG_RETURN_POINTER(TemporalTransform(MAX_TIMESTAMP_PRECISION,
-                                        (Node *) PG_GETARG_POINTER(0)));
+    Node       *rawreq = (Node *) PG_GETARG_POINTER(0);
+    Node       *ret = NULL;
+
+    if (IsA(rawreq, SupportRequestSimplify))
+    {
+        SupportRequestSimplify *req = (SupportRequestSimplify *) rawreq;
+
+        ret = TemporalSimplify(MAX_TIMESTAMP_PRECISION, (Node *) req->fcall);
+    }
+
+    PG_RETURN_POINTER(ret);
 }

 /* timestamp_scale()
@@ -1235,59 +1247,69 @@ intervaltypmodleastfield(int32 typmod)
 }


-/* interval_transform()
+/*
+ * interval_support()
+ *
+ * Planner support function for interval_scale().
+ *
  * Flatten superfluous calls to interval_scale().  The interval typmod is
  * complex to permit accepting and regurgitating all SQL standard variations.
  * For truncation purposes, it boils down to a single, simple granularity.
  */
 Datum
-interval_transform(PG_FUNCTION_ARGS)
+interval_support(PG_FUNCTION_ARGS)
 {
-    FuncExpr   *expr = castNode(FuncExpr, PG_GETARG_POINTER(0));
+    Node       *rawreq = (Node *) PG_GETARG_POINTER(0);
     Node       *ret = NULL;
-    Node       *typmod;

-    Assert(list_length(expr->args) >= 2);
+    if (IsA(rawreq, SupportRequestSimplify))
+    {
+        SupportRequestSimplify *req = (SupportRequestSimplify *) rawreq;
+        FuncExpr   *expr = req->fcall;
+        Node       *typmod;

-    typmod = (Node *) lsecond(expr->args);
+        Assert(list_length(expr->args) >= 2);

-    if (IsA(typmod, Const) &&!((Const *) typmod)->constisnull)
-    {
-        Node       *source = (Node *) linitial(expr->args);
-        int32        new_typmod = DatumGetInt32(((Const *) typmod)->constvalue);
-        bool        noop;
+        typmod = (Node *) lsecond(expr->args);

-        if (new_typmod < 0)
-            noop = true;
-        else
+        if (IsA(typmod, Const) &&!((Const *) typmod)->constisnull)
         {
-            int32        old_typmod = exprTypmod(source);
-            int            old_least_field;
-            int            new_least_field;
-            int            old_precis;
-            int            new_precis;
-
-            old_least_field = intervaltypmodleastfield(old_typmod);
-            new_least_field = intervaltypmodleastfield(new_typmod);
-            if (old_typmod < 0)
-                old_precis = INTERVAL_FULL_PRECISION;
+            Node       *source = (Node *) linitial(expr->args);
+            int32        new_typmod = DatumGetInt32(((Const *) typmod)->constvalue);
+            bool        noop;
+
+            if (new_typmod < 0)
+                noop = true;
             else
-                old_precis = INTERVAL_PRECISION(old_typmod);
-            new_precis = INTERVAL_PRECISION(new_typmod);
-
-            /*
-             * Cast is a no-op if least field stays the same or decreases
-             * while precision stays the same or increases.  But precision,
-             * which is to say, sub-second precision, only affects ranges that
-             * include SECOND.
-             */
-            noop = (new_least_field <= old_least_field) &&
-                (old_least_field > 0 /* SECOND */ ||
-                 new_precis >= MAX_INTERVAL_PRECISION ||
-                 new_precis >= old_precis);
+            {
+                int32        old_typmod = exprTypmod(source);
+                int            old_least_field;
+                int            new_least_field;
+                int            old_precis;
+                int            new_precis;
+
+                old_least_field = intervaltypmodleastfield(old_typmod);
+                new_least_field = intervaltypmodleastfield(new_typmod);
+                if (old_typmod < 0)
+                    old_precis = INTERVAL_FULL_PRECISION;
+                else
+                    old_precis = INTERVAL_PRECISION(old_typmod);
+                new_precis = INTERVAL_PRECISION(new_typmod);
+
+                /*
+                 * Cast is a no-op if least field stays the same or decreases
+                 * while precision stays the same or increases.  But
+                 * precision, which is to say, sub-second precision, only
+                 * affects ranges that include SECOND.
+                 */
+                noop = (new_least_field <= old_least_field) &&
+                    (old_least_field > 0 /* SECOND */ ||
+                     new_precis >= MAX_INTERVAL_PRECISION ||
+                     new_precis >= old_precis);
+            }
+            if (noop)
+                ret = relabel_to_typmod(source, new_typmod);
         }
-        if (noop)
-            ret = relabel_to_typmod(source, new_typmod);
     }

     PG_RETURN_POINTER(ret);
@@ -1359,7 +1381,7 @@ AdjustIntervalForTypmod(Interval *interval, int32 typmod)
          * can't do it consistently.  (We cannot enforce a range limit on the
          * highest expected field, since we do not have any equivalent of
          * SQL's <interval leading field precision>.)  If we ever decide to
-         * revisit this, interval_transform will likely require adjusting.
+         * revisit this, interval_support will likely require adjusting.
          *
          * Note: before PG 8.4 we interpreted a limited set of fields as
          * actually causing a "modulo" operation on a given value, potentially
@@ -5020,18 +5042,6 @@ interval_part(PG_FUNCTION_ARGS)
 }


-/* timestamp_zone_transform()
- * The original optimization here caused problems by relabeling Vars that
- * could be matched to index entries.  It might be possible to resurrect it
- * at some point by teaching the planner to be less cavalier with RelabelType
- * nodes, but that will take careful analysis.
- */
-Datum
-timestamp_zone_transform(PG_FUNCTION_ARGS)
-{
-    PG_RETURN_POINTER(NULL);
-}
-
 /*    timestamp_zone()
  *    Encode timestamp type with specified time zone.
  *    This function is just timestamp2timestamptz() except instead of
@@ -5125,18 +5135,6 @@ timestamp_zone(PG_FUNCTION_ARGS)
     PG_RETURN_TIMESTAMPTZ(result);
 }

-/* timestamp_izone_transform()
- * The original optimization here caused problems by relabeling Vars that
- * could be matched to index entries.  It might be possible to resurrect it
- * at some point by teaching the planner to be less cavalier with RelabelType
- * nodes, but that will take careful analysis.
- */
-Datum
-timestamp_izone_transform(PG_FUNCTION_ARGS)
-{
-    PG_RETURN_POINTER(NULL);
-}
-
 /* timestamp_izone()
  * Encode timestamp type with specified time interval as time zone.
  */
diff --git a/src/backend/utils/adt/varbit.c b/src/backend/utils/adt/varbit.c
index 1585da0..fdcc620 100644
--- a/src/backend/utils/adt/varbit.c
+++ b/src/backend/utils/adt/varbit.c
@@ -20,6 +20,7 @@
 #include "common/int.h"
 #include "libpq/pqformat.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/supportnodes.h"
 #include "utils/array.h"
 #include "utils/builtins.h"
 #include "utils/varbit.h"
@@ -672,32 +673,41 @@ varbit_send(PG_FUNCTION_ARGS)
 }

 /*
- * varbit_transform()
- * Flatten calls to varbit's length coercion function that set the new maximum
- * length >= the previous maximum length.  We can ignore the isExplicit
- * argument, since that only affects truncation cases.
+ * varbit_support()
+ *
+ * Planner support function for the varbit() length coercion function.
+ *
+ * Currently, the only interesting thing we can do is flatten calls that set
+ * the new maximum length >= the previous maximum length.  We can ignore the
+ * isExplicit argument, since that only affects truncation cases.
  */
 Datum
-varbit_transform(PG_FUNCTION_ARGS)
+varbit_support(PG_FUNCTION_ARGS)
 {
-    FuncExpr   *expr = castNode(FuncExpr, PG_GETARG_POINTER(0));
+    Node       *rawreq = (Node *) PG_GETARG_POINTER(0);
     Node       *ret = NULL;
-    Node       *typmod;

-    Assert(list_length(expr->args) >= 2);
+    if (IsA(rawreq, SupportRequestSimplify))
+    {
+        SupportRequestSimplify *req = (SupportRequestSimplify *) rawreq;
+        FuncExpr   *expr = req->fcall;
+        Node       *typmod;

-    typmod = (Node *) lsecond(expr->args);
+        Assert(list_length(expr->args) >= 2);

-    if (IsA(typmod, Const) &&!((Const *) typmod)->constisnull)
-    {
-        Node       *source = (Node *) linitial(expr->args);
-        int32        new_typmod = DatumGetInt32(((Const *) typmod)->constvalue);
-        int32        old_max = exprTypmod(source);
-        int32        new_max = new_typmod;
-
-        /* Note: varbit() treats typmod 0 as invalid, so we do too */
-        if (new_max <= 0 || (old_max > 0 && old_max <= new_max))
-            ret = relabel_to_typmod(source, new_typmod);
+        typmod = (Node *) lsecond(expr->args);
+
+        if (IsA(typmod, Const) &&!((Const *) typmod)->constisnull)
+        {
+            Node       *source = (Node *) linitial(expr->args);
+            int32        new_typmod = DatumGetInt32(((Const *) typmod)->constvalue);
+            int32        old_max = exprTypmod(source);
+            int32        new_max = new_typmod;
+
+            /* Note: varbit() treats typmod 0 as invalid, so we do too */
+            if (new_max <= 0 || (old_max > 0 && old_max <= new_max))
+                ret = relabel_to_typmod(source, new_typmod);
+        }
     }

     PG_RETURN_POINTER(ret);
diff --git a/src/backend/utils/adt/varchar.c b/src/backend/utils/adt/varchar.c
index 5cf927e..c866af0 100644
--- a/src/backend/utils/adt/varchar.c
+++ b/src/backend/utils/adt/varchar.c
@@ -21,6 +21,7 @@
 #include "catalog/pg_type.h"
 #include "libpq/pqformat.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/supportnodes.h"
 #include "utils/array.h"
 #include "utils/builtins.h"
 #include "utils/varlena.h"
@@ -547,32 +548,41 @@ varcharsend(PG_FUNCTION_ARGS)


 /*
- * varchar_transform()
- * Flatten calls to varchar's length coercion function that set the new maximum
- * length >= the previous maximum length.  We can ignore the isExplicit
- * argument, since that only affects truncation cases.
+ * varchar_support()
+ *
+ * Planner support function for the varchar() length coercion function.
+ *
+ * Currently, the only interesting thing we can do is flatten calls that set
+ * the new maximum length >= the previous maximum length.  We can ignore the
+ * isExplicit argument, since that only affects truncation cases.
  */
 Datum
-varchar_transform(PG_FUNCTION_ARGS)
+varchar_support(PG_FUNCTION_ARGS)
 {
-    FuncExpr   *expr = castNode(FuncExpr, PG_GETARG_POINTER(0));
+    Node       *rawreq = (Node *) PG_GETARG_POINTER(0);
     Node       *ret = NULL;
-    Node       *typmod;

-    Assert(list_length(expr->args) >= 2);
+    if (IsA(rawreq, SupportRequestSimplify))
+    {
+        SupportRequestSimplify *req = (SupportRequestSimplify *) rawreq;
+        FuncExpr   *expr = req->fcall;
+        Node       *typmod;

-    typmod = (Node *) lsecond(expr->args);
+        Assert(list_length(expr->args) >= 2);

-    if (IsA(typmod, Const) &&!((Const *) typmod)->constisnull)
-    {
-        Node       *source = (Node *) linitial(expr->args);
-        int32        old_typmod = exprTypmod(source);
-        int32        new_typmod = DatumGetInt32(((Const *) typmod)->constvalue);
-        int32        old_max = old_typmod - VARHDRSZ;
-        int32        new_max = new_typmod - VARHDRSZ;
-
-        if (new_typmod < 0 || (old_typmod >= 0 && old_max <= new_max))
-            ret = relabel_to_typmod(source, new_typmod);
+        typmod = (Node *) lsecond(expr->args);
+
+        if (IsA(typmod, Const) &&!((Const *) typmod)->constisnull)
+        {
+            Node       *source = (Node *) linitial(expr->args);
+            int32        old_typmod = exprTypmod(source);
+            int32        new_typmod = DatumGetInt32(((Const *) typmod)->constvalue);
+            int32        old_max = old_typmod - VARHDRSZ;
+            int32        new_max = new_typmod - VARHDRSZ;
+
+            if (new_typmod < 0 || (old_typmod >= 0 && old_max <= new_max))
+                ret = relabel_to_typmod(source, new_typmod);
+        }
     }

     PG_RETURN_POINTER(ret);
diff --git a/src/bin/pg_dump/t/002_pg_dump.pl b/src/bin/pg_dump/t/002_pg_dump.pl
index 245fcbf..4ff358a 100644
--- a/src/bin/pg_dump/t/002_pg_dump.pl
+++ b/src/bin/pg_dump/t/002_pg_dump.pl
@@ -1883,9 +1883,9 @@ my %tests = (
     'CREATE TRANSFORM FOR int' => {
         create_order => 34,
         create_sql =>
-          'CREATE TRANSFORM FOR int LANGUAGE SQL (FROM SQL WITH FUNCTION varchar_transform(internal), TO SQL WITH
FUNCTIONint4recv(internal));', 
+          'CREATE TRANSFORM FOR int LANGUAGE SQL (FROM SQL WITH FUNCTION varchar_support(internal), TO SQL WITH
FUNCTIONint4recv(internal));', 
         regexp =>
-          qr/CREATE TRANSFORM FOR integer LANGUAGE sql \(FROM SQL WITH FUNCTION
pg_catalog\.varchar_transform\(internal\),TO SQL WITH FUNCTION pg_catalog\.int4recv\(internal\)\);/m, 
+          qr/CREATE TRANSFORM FOR integer LANGUAGE sql \(FROM SQL WITH FUNCTION
pg_catalog\.varchar_support\(internal\),TO SQL WITH FUNCTION pg_catalog\.int4recv\(internal\)\);/m, 
         like => { %full_runs, section_pre_data => 1, },
     },

@@ -2880,7 +2880,7 @@ my %tests = (
                            procost,
                            prorows,
                            provariadic,
-                           protransform,
+                           prosupport,
                            prokind,
                            prosecdef,
                            proleakproof,
@@ -2912,7 +2912,7 @@ my %tests = (
         \QGRANT SELECT(procost) ON TABLE pg_catalog.pg_proc TO PUBLIC;\E\n.*
         \QGRANT SELECT(prorows) ON TABLE pg_catalog.pg_proc TO PUBLIC;\E\n.*
         \QGRANT SELECT(provariadic) ON TABLE pg_catalog.pg_proc TO PUBLIC;\E\n.*
-        \QGRANT SELECT(protransform) ON TABLE pg_catalog.pg_proc TO PUBLIC;\E\n.*
+        \QGRANT SELECT(prosupport) ON TABLE pg_catalog.pg_proc TO PUBLIC;\E\n.*
         \QGRANT SELECT(prokind) ON TABLE pg_catalog.pg_proc TO PUBLIC;\E\n.*
         \QGRANT SELECT(prosecdef) ON TABLE pg_catalog.pg_proc TO PUBLIC;\E\n.*
         \QGRANT SELECT(proleakproof) ON TABLE pg_catalog.pg_proc TO PUBLIC;\E\n.*
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3ecc2e1..e5cb5bb 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -1326,11 +1326,11 @@
 { oid => '668', descr => 'adjust char() to typmod length',
   proname => 'bpchar', prorettype => 'bpchar',
   proargtypes => 'bpchar int4 bool', prosrc => 'bpchar' },
-{ oid => '3097', descr => 'transform a varchar length coercion',
-  proname => 'varchar_transform', prorettype => 'internal',
-  proargtypes => 'internal', prosrc => 'varchar_transform' },
+{ oid => '3097', descr => 'planner support for varchar length coercion',
+  proname => 'varchar_support', prorettype => 'internal',
+  proargtypes => 'internal', prosrc => 'varchar_support' },
 { oid => '669', descr => 'adjust varchar() to typmod length',
-  proname => 'varchar', protransform => 'varchar_transform',
+  proname => 'varchar', prosupport => 'varchar_support',
   prorettype => 'varchar', proargtypes => 'varchar int4 bool',
   prosrc => 'varchar' },

@@ -1954,13 +1954,9 @@

 # OIDS 1000 - 1999

-{ oid => '3994', descr => 'transform a time zone adjustment',
-  proname => 'timestamp_izone_transform', prorettype => 'internal',
-  proargtypes => 'internal', prosrc => 'timestamp_izone_transform' },
 { oid => '1026', descr => 'adjust timestamp to new time zone',
-  proname => 'timezone', protransform => 'timestamp_izone_transform',
-  prorettype => 'timestamp', proargtypes => 'interval timestamptz',
-  prosrc => 'timestamptz_izone' },
+  proname => 'timezone', prorettype => 'timestamp',
+  proargtypes => 'interval timestamptz', prosrc => 'timestamptz_izone' },

 { oid => '1031', descr => 'I/O',
   proname => 'aclitemin', provolatile => 's', prorettype => 'aclitem',
@@ -2190,13 +2186,9 @@
 { oid => '1158', descr => 'convert UNIX epoch to timestamptz',
   proname => 'to_timestamp', prorettype => 'timestamptz',
   proargtypes => 'float8', prosrc => 'float8_timestamptz' },
-{ oid => '3995', descr => 'transform a time zone adjustment',
-  proname => 'timestamp_zone_transform', prorettype => 'internal',
-  proargtypes => 'internal', prosrc => 'timestamp_zone_transform' },
 { oid => '1159', descr => 'adjust timestamp to new time zone',
-  proname => 'timezone', protransform => 'timestamp_zone_transform',
-  prorettype => 'timestamp', proargtypes => 'text timestamptz',
-  prosrc => 'timestamptz_zone' },
+  proname => 'timezone', prorettype => 'timestamp',
+  proargtypes => 'text timestamptz', prosrc => 'timestamptz_zone' },

 { oid => '1160', descr => 'I/O',
   proname => 'interval_in', provolatile => 's', prorettype => 'interval',
@@ -2301,11 +2293,11 @@

 # OIDS 1200 - 1299

-{ oid => '3918', descr => 'transform an interval length coercion',
-  proname => 'interval_transform', prorettype => 'internal',
-  proargtypes => 'internal', prosrc => 'interval_transform' },
+{ oid => '3918', descr => 'planner support for interval length coercion',
+  proname => 'interval_support', prorettype => 'internal',
+  proargtypes => 'internal', prosrc => 'interval_support' },
 { oid => '1200', descr => 'adjust interval precision',
-  proname => 'interval', protransform => 'interval_transform',
+  proname => 'interval', prosupport => 'interval_support',
   prorettype => 'interval', proargtypes => 'interval int4',
   prosrc => 'interval_scale' },

@@ -3713,13 +3705,12 @@
 { oid => '1685', descr => 'adjust bit() to typmod length',
   proname => 'bit', prorettype => 'bit', proargtypes => 'bit int4 bool',
   prosrc => 'bit' },
-{ oid => '3158', descr => 'transform a varbit length coercion',
-  proname => 'varbit_transform', prorettype => 'internal',
-  proargtypes => 'internal', prosrc => 'varbit_transform' },
+{ oid => '3158', descr => 'planner support for varbit length coercion',
+  proname => 'varbit_support', prorettype => 'internal',
+  proargtypes => 'internal', prosrc => 'varbit_support' },
 { oid => '1687', descr => 'adjust varbit() to typmod length',
-  proname => 'varbit', protransform => 'varbit_transform',
-  prorettype => 'varbit', proargtypes => 'varbit int4 bool',
-  prosrc => 'varbit' },
+  proname => 'varbit', prosupport => 'varbit_support', prorettype => 'varbit',
+  proargtypes => 'varbit int4 bool', prosrc => 'varbit' },

 { oid => '1698', descr => 'position of sub-bitstring',
   proname => 'position', prorettype => 'int4', proargtypes => 'bit bit',
@@ -4081,11 +4072,11 @@
 { oid => '2918', descr => 'I/O typmod',
   proname => 'numerictypmodout', prorettype => 'cstring', proargtypes => 'int4',
   prosrc => 'numerictypmodout' },
-{ oid => '3157', descr => 'transform a numeric length coercion',
-  proname => 'numeric_transform', prorettype => 'internal',
-  proargtypes => 'internal', prosrc => 'numeric_transform' },
+{ oid => '3157', descr => 'planner support for numeric length coercion',
+  proname => 'numeric_support', prorettype => 'internal',
+  proargtypes => 'internal', prosrc => 'numeric_support' },
 { oid => '1703', descr => 'adjust numeric to typmod precision/scale',
-  proname => 'numeric', protransform => 'numeric_transform',
+  proname => 'numeric', prosupport => 'numeric_support',
   prorettype => 'numeric', proargtypes => 'numeric int4', prosrc => 'numeric' },
 { oid => '1704',
   proname => 'numeric_abs', prorettype => 'numeric', proargtypes => 'numeric',
@@ -5448,15 +5439,15 @@
   proname => 'bytea_sortsupport', prorettype => 'void',
   proargtypes => 'internal', prosrc => 'bytea_sortsupport' },

-{ oid => '3917', descr => 'transform a timestamp length coercion',
-  proname => 'timestamp_transform', prorettype => 'internal',
-  proargtypes => 'internal', prosrc => 'timestamp_transform' },
-{ oid => '3944', descr => 'transform a time length coercion',
-  proname => 'time_transform', prorettype => 'internal',
-  proargtypes => 'internal', prosrc => 'time_transform' },
+{ oid => '3917', descr => 'planner support for timestamp length coercion',
+  proname => 'timestamp_support', prorettype => 'internal',
+  proargtypes => 'internal', prosrc => 'timestamp_support' },
+{ oid => '3944', descr => 'planner support for time length coercion',
+  proname => 'time_support', prorettype => 'internal',
+  proargtypes => 'internal', prosrc => 'time_support' },

 { oid => '1961', descr => 'adjust timestamp precision',
-  proname => 'timestamp', protransform => 'timestamp_transform',
+  proname => 'timestamp', prosupport => 'timestamp_support',
   prorettype => 'timestamp', proargtypes => 'timestamp int4',
   prosrc => 'timestamp_scale' },

@@ -5468,14 +5459,14 @@
   prosrc => 'oidsmaller' },

 { oid => '1967', descr => 'adjust timestamptz precision',
-  proname => 'timestamptz', protransform => 'timestamp_transform',
+  proname => 'timestamptz', prosupport => 'timestamp_support',
   prorettype => 'timestamptz', proargtypes => 'timestamptz int4',
   prosrc => 'timestamptz_scale' },
 { oid => '1968', descr => 'adjust time precision',
-  proname => 'time', protransform => 'time_transform', prorettype => 'time',
+  proname => 'time', prosupport => 'time_support', prorettype => 'time',
   proargtypes => 'time int4', prosrc => 'time_scale' },
 { oid => '1969', descr => 'adjust time with time zone precision',
-  proname => 'timetz', protransform => 'time_transform', prorettype => 'timetz',
+  proname => 'timetz', prosupport => 'time_support', prorettype => 'timetz',
   proargtypes => 'timetz int4', prosrc => 'timetz_scale' },

 { oid => '2003',
@@ -5662,13 +5653,11 @@
   prosrc => 'select pg_catalog.age(cast(current_date as timestamp without time zone), $1)' },

 { oid => '2069', descr => 'adjust timestamp to new time zone',
-  proname => 'timezone', protransform => 'timestamp_zone_transform',
-  prorettype => 'timestamptz', proargtypes => 'text timestamp',
-  prosrc => 'timestamp_zone' },
+  proname => 'timezone', prorettype => 'timestamptz',
+  proargtypes => 'text timestamp', prosrc => 'timestamp_zone' },
 { oid => '2070', descr => 'adjust timestamp to new time zone',
-  proname => 'timezone', protransform => 'timestamp_izone_transform',
-  prorettype => 'timestamptz', proargtypes => 'interval timestamp',
-  prosrc => 'timestamp_izone' },
+  proname => 'timezone', prorettype => 'timestamptz',
+  proargtypes => 'interval timestamp', prosrc => 'timestamp_izone' },
 { oid => '2071',
   proname => 'date_pl_interval', prorettype => 'timestamp',
   proargtypes => 'date interval', prosrc => 'date_pl_interval' },
diff --git a/src/include/catalog/pg_proc.h b/src/include/catalog/pg_proc.h
index c2bb951..b433769 100644
--- a/src/include/catalog/pg_proc.h
+++ b/src/include/catalog/pg_proc.h
@@ -53,8 +53,8 @@ CATALOG(pg_proc,1255,ProcedureRelationId) BKI_BOOTSTRAP BKI_ROWTYPE_OID(81,Proce
     /* element type of variadic array, or 0 */
     Oid            provariadic BKI_DEFAULT(0) BKI_LOOKUP(pg_type);

-    /* transforms calls to it during planning */
-    regproc        protransform BKI_DEFAULT(0) BKI_LOOKUP(pg_proc);
+    /* planner support function for this function, or 0 if none */
+    regproc        prosupport BKI_DEFAULT(0) BKI_LOOKUP(pg_proc);

     /* see PROKIND_ categories below */
     char        prokind BKI_DEFAULT(f);
diff --git a/src/include/nodes/nodes.h b/src/include/nodes/nodes.h
index 10dac60..e029b40 100644
--- a/src/include/nodes/nodes.h
+++ b/src/include/nodes/nodes.h
@@ -505,7 +505,8 @@ typedef enum NodeTag
     T_IndexAmRoutine,            /* in access/amapi.h */
     T_TsmRoutine,                /* in access/tsmapi.h */
     T_ForeignKeyCacheInfo,        /* in utils/rel.h */
-    T_CallContext                /* in nodes/parsenodes.h */
+    T_CallContext,                /* in nodes/parsenodes.h */
+    T_SupportRequestSimplify    /* in nodes/supportnodes.h */
 } NodeTag;

 /*
diff --git a/src/include/nodes/supportnodes.h b/src/include/nodes/supportnodes.h
new file mode 100644
index 0000000..1f7d02b
--- /dev/null
+++ b/src/include/nodes/supportnodes.h
@@ -0,0 +1,70 @@
+/*-------------------------------------------------------------------------
+ *
+ * supportnodes.h
+ *      Definitions for planner support functions.
+ *
+ * This file defines the API for "planner support functions", which
+ * are SQL functions (normally written in C) that can be attached to
+ * another "target" function to give the system additional knowledge
+ * about the target function.  All the current capabilities have to do
+ * with planning queries that use the target function, though it is
+ * possible that future extensions will add functionality to be invoked
+ * by the parser or executor.
+ *
+ * A support function must have the SQL signature
+ *        supportfn(internal) returns internal
+ * The argument is a pointer to one of the Node types defined in this file.
+ * The result is usually also a Node pointer, though its type depends on
+ * which capability is being invoked.  In all cases, a NULL pointer result
+ * (that's PG_RETURN_POINTER(NULL), not PG_RETURN_NULL()) indicates that
+ * the support function cannot do anything useful for the given request.
+ * Support functions must return a NULL pointer, not fail, if they do not
+ * recognize the request node type or cannot handle the given case; this
+ * allows for future extensions of the set of request cases.
+ *
+ *
+ * Portions Copyright (c) 1996-2019, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/nodes/supportnodes.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef SUPPORTNODES_H
+#define SUPPORTNODES_H
+
+#include "nodes/primnodes.h"
+
+struct PlannerInfo;                /* avoid including relation.h here */
+
+
+/*
+ * The Simplify request allows the support function to perform plan-time
+ * simplification of a call to its target function.  For example, a varchar
+ * length coercion that does not decrease the allowed length of its argument
+ * could be replaced by a RelabelType node, or "x + 0" could be replaced by
+ * "x".  This is invoked during the planner's constant-folding pass, so the
+ * function's arguments can be presumed already simplified.
+ *
+ * The planner's PlannerInfo "root" is typically not needed, but can be
+ * consulted if it's necessary to obtain info about Vars present in
+ * the given node tree.  Beware that root could be NULL in some usages.
+ *
+ * "fcall" will be a FuncExpr invoking the support function's target
+ * function.  (This is true even if the original parsetree node was an
+ * operator call; a FuncExpr is synthesized for this purpose.)
+ *
+ * The result should be a semantically-equivalent transformed node tree,
+ * or NULL if no simplification could be performed.  Do *not* return or
+ * modify *fcall, as it isn't really a separately allocated Node.  But
+ * it's okay to use fcall->args, or parts of it, in the result tree.
+ */
+typedef struct SupportRequestSimplify
+{
+    NodeTag        type;
+
+    struct PlannerInfo *root;    /* Planner's infrastructure */
+    FuncExpr   *fcall;            /* Function call to be simplified */
+} SupportRequestSimplify;
+
+#endif                            /* SUPPORTNODES_H */
diff --git a/src/include/utils/datetime.h b/src/include/utils/datetime.h
index f5ec9bb..87f819e 100644
--- a/src/include/utils/datetime.h
+++ b/src/include/utils/datetime.h
@@ -330,7 +330,7 @@ extern int    DecodeUnits(int field, char *lowtoken, int *val);

 extern int    j2day(int jd);

-extern Node *TemporalTransform(int32 max_precis, Node *node);
+extern Node *TemporalSimplify(int32 max_precis, Node *node);

 extern bool CheckDateTokenTables(void);

diff --git a/src/test/modules/test_ddl_deparse/expected/create_transform.out
b/src/test/modules/test_ddl_deparse/expected/create_transform.out
index 0d1cc36..da7fea2 100644
--- a/src/test/modules/test_ddl_deparse/expected/create_transform.out
+++ b/src/test/modules/test_ddl_deparse/expected/create_transform.out
@@ -7,7 +7,7 @@
 -- internal and as return argument the datatype of the transform done.
 -- pl/plpgsql does not authorize the use of internal as data type.
 CREATE TRANSFORM FOR int LANGUAGE SQL (
-    FROM SQL WITH FUNCTION varchar_transform(internal),
+    FROM SQL WITH FUNCTION varchar_support(internal),
     TO SQL WITH FUNCTION int4recv(internal));
 NOTICE:  DDL test: type simple, tag CREATE TRANSFORM
 DROP TRANSFORM FOR int LANGUAGE SQL;
diff --git a/src/test/modules/test_ddl_deparse/sql/create_transform.sql
b/src/test/modules/test_ddl_deparse/sql/create_transform.sql
index 0968702..132fc5a 100644
--- a/src/test/modules/test_ddl_deparse/sql/create_transform.sql
+++ b/src/test/modules/test_ddl_deparse/sql/create_transform.sql
@@ -8,7 +8,7 @@
 -- internal and as return argument the datatype of the transform done.
 -- pl/plpgsql does not authorize the use of internal as data type.
 CREATE TRANSFORM FOR int LANGUAGE SQL (
-    FROM SQL WITH FUNCTION varchar_transform(internal),
+    FROM SQL WITH FUNCTION varchar_support(internal),
     TO SQL WITH FUNCTION int4recv(internal));

 DROP TRANSFORM FOR int LANGUAGE SQL;
diff --git a/src/test/regress/expected/object_address.out b/src/test/regress/expected/object_address.out
index 4085e45..c89ec06 100644
--- a/src/test/regress/expected/object_address.out
+++ b/src/test/regress/expected/object_address.out
@@ -38,7 +38,7 @@ CREATE USER MAPPING FOR regress_addr_user SERVER "integer";
 ALTER DEFAULT PRIVILEGES FOR ROLE regress_addr_user IN SCHEMA public GRANT ALL ON TABLES TO regress_addr_user;
 ALTER DEFAULT PRIVILEGES FOR ROLE regress_addr_user REVOKE DELETE ON TABLES FROM regress_addr_user;
 CREATE TRANSFORM FOR int LANGUAGE SQL (
-    FROM SQL WITH FUNCTION varchar_transform(internal),
+    FROM SQL WITH FUNCTION varchar_support(internal),
     TO SQL WITH FUNCTION int4recv(internal));
 CREATE PUBLICATION addr_pub FOR TABLE addr_nsp.gentable;
 CREATE SUBSCRIPTION addr_sub CONNECTION '' PUBLICATION bar WITH (connect = false, slot_name = NONE);
diff --git a/src/test/regress/expected/oidjoins.out b/src/test/regress/expected/oidjoins.out
index ef268d3..4edc817 100644
--- a/src/test/regress/expected/oidjoins.out
+++ b/src/test/regress/expected/oidjoins.out
@@ -809,12 +809,12 @@ WHERE    provariadic != 0 AND
 ------+-------------
 (0 rows)

-SELECT    ctid, protransform
+SELECT    ctid, prosupport
 FROM    pg_catalog.pg_proc fk
-WHERE    protransform != 0 AND
-    NOT EXISTS(SELECT 1 FROM pg_catalog.pg_proc pk WHERE pk.oid = fk.protransform);
- ctid | protransform
-------+--------------
+WHERE    prosupport != 0 AND
+    NOT EXISTS(SELECT 1 FROM pg_catalog.pg_proc pk WHERE pk.oid = fk.prosupport);
+ ctid | prosupport
+------+------------
 (0 rows)

 SELECT    ctid, prorettype
diff --git a/src/test/regress/expected/opr_sanity.out b/src/test/regress/expected/opr_sanity.out
index 7328095..ce25ee0 100644
--- a/src/test/regress/expected/opr_sanity.out
+++ b/src/test/regress/expected/opr_sanity.out
@@ -453,10 +453,10 @@ WHERE proallargtypes IS NOT NULL AND
 -----+---------+-------------+----------------+-------------
 (0 rows)

--- Check for protransform functions with the wrong signature
+-- Check for prosupport functions with the wrong signature
 SELECT p1.oid, p1.proname, p2.oid, p2.proname
 FROM pg_proc AS p1, pg_proc AS p2
-WHERE p2.oid = p1.protransform AND
+WHERE p2.oid = p1.prosupport AND
     (p2.prorettype != 'internal'::regtype OR p2.proretset OR p2.pronargs != 1
      OR p2.proargtypes[0] != 'internal'::regtype);
  oid | proname | oid | proname
diff --git a/src/test/regress/sql/object_address.sql b/src/test/regress/sql/object_address.sql
index d7df322..fd79465 100644
--- a/src/test/regress/sql/object_address.sql
+++ b/src/test/regress/sql/object_address.sql
@@ -41,7 +41,7 @@ CREATE USER MAPPING FOR regress_addr_user SERVER "integer";
 ALTER DEFAULT PRIVILEGES FOR ROLE regress_addr_user IN SCHEMA public GRANT ALL ON TABLES TO regress_addr_user;
 ALTER DEFAULT PRIVILEGES FOR ROLE regress_addr_user REVOKE DELETE ON TABLES FROM regress_addr_user;
 CREATE TRANSFORM FOR int LANGUAGE SQL (
-    FROM SQL WITH FUNCTION varchar_transform(internal),
+    FROM SQL WITH FUNCTION varchar_support(internal),
     TO SQL WITH FUNCTION int4recv(internal));
 CREATE PUBLICATION addr_pub FOR TABLE addr_nsp.gentable;
 CREATE SUBSCRIPTION addr_sub CONNECTION '' PUBLICATION bar WITH (connect = false, slot_name = NONE);
diff --git a/src/test/regress/sql/oidjoins.sql b/src/test/regress/sql/oidjoins.sql
index c8291d3..dbe4a58 100644
--- a/src/test/regress/sql/oidjoins.sql
+++ b/src/test/regress/sql/oidjoins.sql
@@ -405,10 +405,10 @@ SELECT    ctid, provariadic
 FROM    pg_catalog.pg_proc fk
 WHERE    provariadic != 0 AND
     NOT EXISTS(SELECT 1 FROM pg_catalog.pg_type pk WHERE pk.oid = fk.provariadic);
-SELECT    ctid, protransform
+SELECT    ctid, prosupport
 FROM    pg_catalog.pg_proc fk
-WHERE    protransform != 0 AND
-    NOT EXISTS(SELECT 1 FROM pg_catalog.pg_proc pk WHERE pk.oid = fk.protransform);
+WHERE    prosupport != 0 AND
+    NOT EXISTS(SELECT 1 FROM pg_catalog.pg_proc pk WHERE pk.oid = fk.prosupport);
 SELECT    ctid, prorettype
 FROM    pg_catalog.pg_proc fk
 WHERE    prorettype != 0 AND
diff --git a/src/test/regress/sql/opr_sanity.sql b/src/test/regress/sql/opr_sanity.sql
index 8544cbe..e2014fc 100644
--- a/src/test/regress/sql/opr_sanity.sql
+++ b/src/test/regress/sql/opr_sanity.sql
@@ -353,10 +353,10 @@ WHERE proallargtypes IS NOT NULL AND
         FROM generate_series(1, array_length(proallargtypes, 1)) g(i)
         WHERE proargmodes IS NULL OR proargmodes[i] IN ('i', 'b', 'v'));

--- Check for protransform functions with the wrong signature
+-- Check for prosupport functions with the wrong signature
 SELECT p1.oid, p1.proname, p2.oid, p2.proname
 FROM pg_proc AS p1, pg_proc AS p2
-WHERE p2.oid = p1.protransform AND
+WHERE p2.oid = p1.prosupport AND
     (p2.prorettype != 'internal'::regtype OR p2.proretset OR p2.pronargs != 1
      OR p2.proargtypes[0] != 'internal'::regtype);

diff --git a/src/tools/findoidjoins/README b/src/tools/findoidjoins/README
index 305454a..e5fc310 100644
--- a/src/tools/findoidjoins/README
+++ b/src/tools/findoidjoins/README
@@ -161,7 +161,7 @@ Join pg_catalog.pg_proc.pronamespace => pg_catalog.pg_namespace.oid
 Join pg_catalog.pg_proc.proowner => pg_catalog.pg_authid.oid
 Join pg_catalog.pg_proc.prolang => pg_catalog.pg_language.oid
 Join pg_catalog.pg_proc.provariadic => pg_catalog.pg_type.oid
-Join pg_catalog.pg_proc.protransform => pg_catalog.pg_proc.oid
+Join pg_catalog.pg_proc.prosupport => pg_catalog.pg_proc.oid
 Join pg_catalog.pg_proc.prorettype => pg_catalog.pg_type.oid
 Join pg_catalog.pg_range.rngtypid => pg_catalog.pg_type.oid
 Join pg_catalog.pg_range.rngsubtype => pg_catalog.pg_type.oid
diff --git a/doc/src/sgml/keywords.sgml b/doc/src/sgml/keywords.sgml
index a37d0b7..fa32a88 100644
--- a/doc/src/sgml/keywords.sgml
+++ b/doc/src/sgml/keywords.sgml
@@ -4522,6 +4522,13 @@
     <entry>reserved</entry>
    </row>
    <row>
+    <entry><token>SUPPORT</token></entry>
+    <entry>non-reserved</entry>
+    <entry></entry>
+    <entry></entry>
+    <entry></entry>
+   </row>
+   <row>
     <entry><token>SYMMETRIC</token></entry>
     <entry>reserved</entry>
     <entry>reserved</entry>
diff --git a/doc/src/sgml/ref/alter_function.sgml b/doc/src/sgml/ref/alter_function.sgml
index d8747e0..03ffa59 100644
--- a/doc/src/sgml/ref/alter_function.sgml
+++ b/doc/src/sgml/ref/alter_function.sgml
@@ -40,6 +40,7 @@ ALTER FUNCTION <replaceable>name</replaceable> [ ( [ [ <replaceable class="param
     PARALLEL { UNSAFE | RESTRICTED | SAFE }
     COST <replaceable class="parameter">execution_cost</replaceable>
     ROWS <replaceable class="parameter">result_rows</replaceable>
+    SUPPORT <replaceable class="parameter">support_function</replaceable>
     SET <replaceable class="parameter">configuration_parameter</replaceable> { TO | = } { <replaceable
class="parameter">value</replaceable>| DEFAULT } 
     SET <replaceable class="parameter">configuration_parameter</replaceable> FROM CURRENT
     RESET <replaceable class="parameter">configuration_parameter</replaceable>
@@ -248,6 +249,24 @@ ALTER FUNCTION <replaceable>name</replaceable> [ ( [ [ <replaceable class="param
      </listitem>
    </varlistentry>

+   <varlistentry>
+    <term><literal>SUPPORT</literal> <replaceable class="parameter">support_function</replaceable></term>
+
+    <listitem>
+     <para>
+      Set or change the planner support function to use for this function.
+      See <xref linkend="xfunc-optimization"/> for details.  You must be
+      superuser to use this option.
+     </para>
+
+     <para>
+      This option cannot be used to remove the support function altogether,
+      since it must name a new support function.  Use <command>CREATE OR
+      REPLACE FUNCTION</command> if you need to do that.
+     </para>
+    </listitem>
+   </varlistentry>
+
      <varlistentry>
       <term><replaceable>configuration_parameter</replaceable></term>
       <term><replaceable>value</replaceable></term>
diff --git a/doc/src/sgml/ref/create_function.sgml b/doc/src/sgml/ref/create_function.sgml
index 4072543..dd6a2f7 100644
--- a/doc/src/sgml/ref/create_function.sgml
+++ b/doc/src/sgml/ref/create_function.sgml
@@ -33,6 +33,7 @@ CREATE [ OR REPLACE ] FUNCTION
     | PARALLEL { UNSAFE | RESTRICTED | SAFE }
     | COST <replaceable class="parameter">execution_cost</replaceable>
     | ROWS <replaceable class="parameter">result_rows</replaceable>
+    | SUPPORT <replaceable class="parameter">support_function</replaceable>
     | SET <replaceable class="parameter">configuration_parameter</replaceable> { TO <replaceable
class="parameter">value</replaceable>| = <replaceable class="parameter">value</replaceable> | FROM CURRENT } 
     | AS '<replaceable class="parameter">definition</replaceable>'
     | AS '<replaceable class="parameter">obj_file</replaceable>', '<replaceable
class="parameter">link_symbol</replaceable>'
@@ -478,6 +479,19 @@ CREATE [ OR REPLACE ] FUNCTION
     </varlistentry>

     <varlistentry>
+     <term><literal>SUPPORT</literal> <replaceable class="parameter">support_function</replaceable></term>
+
+     <listitem>
+      <para>
+       The name (optionally schema-qualified) of a <firstterm>planner support
+       function</firstterm> to use for this function.  See
+       <xref linkend="xfunc-optimization"/> for details.
+       You must be superuser to use this option.
+      </para>
+     </listitem>
+    </varlistentry>
+
+    <varlistentry>
      <term><replaceable>configuration_parameter</replaceable></term>
      <term><replaceable>value</replaceable></term>
      <listitem>
diff --git a/src/backend/catalog/pg_aggregate.c b/src/backend/catalog/pg_aggregate.c
index cc3806e..19e3171 100644
--- a/src/backend/catalog/pg_aggregate.c
+++ b/src/backend/catalog/pg_aggregate.c
@@ -632,6 +632,7 @@ AggregateCreate(const char *aggName,
                              parameterDefaults, /* parameterDefaults */
                              PointerGetDatum(NULL), /* trftypes */
                              PointerGetDatum(NULL), /* proconfig */
+                             InvalidOid,    /* no prosupport */
                              1, /* procost */
                              0);    /* prorows */
     procOid = myself.objectId;
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 2b8f651..23b01f8 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -286,9 +286,12 @@ deleteDependencyRecordsForClass(Oid classId, Oid objectId,
  * newRefObjectId is the new referenced object (must be of class refClassId).
  *
  * Note the lack of objsubid parameters.  If there are subobject references
- * they will all be readjusted.
+ * they will all be readjusted.  Also, there is an expectation that we are
+ * dealing with NORMAL dependencies: if we have to replace an (implicit)
+ * dependency on a pinned object with an explicit dependency on an unpinned
+ * one, the new one will be NORMAL.
  *
- * Returns the number of records updated.
+ * Returns the number of records updated -- zero indicates a problem.
  */
 long
 changeDependencyFor(Oid classId, Oid objectId,
@@ -301,35 +304,52 @@ changeDependencyFor(Oid classId, Oid objectId,
     SysScanDesc scan;
     HeapTuple    tup;
     ObjectAddress objAddr;
+    ObjectAddress depAddr;
+    bool        oldIsPinned;
     bool        newIsPinned;

     depRel = table_open(DependRelationId, RowExclusiveLock);

     /*
-     * If oldRefObjectId is pinned, there won't be any dependency entries on
-     * it --- we can't cope in that case.  (This isn't really worth expending
-     * code to fix, in current usage; it just means you can't rename stuff out
-     * of pg_catalog, which would likely be a bad move anyway.)
+     * Check to see if either oldRefObjectId or newRefObjectId is pinned.
+     * Pinned objects should not have any dependency entries pointing to them,
+     * so in these cases we should add or remove a pg_depend entry, or do
+     * nothing at all, rather than update an entry as in the normal case.
      */
     objAddr.classId = refClassId;
     objAddr.objectId = oldRefObjectId;
     objAddr.objectSubId = 0;

-    if (isObjectPinned(&objAddr, depRel))
-        ereport(ERROR,
-                (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-                 errmsg("cannot remove dependency on %s because it is a system object",
-                        getObjectDescription(&objAddr))));
+    oldIsPinned = isObjectPinned(&objAddr, depRel);

-    /*
-     * We can handle adding a dependency on something pinned, though, since
-     * that just means deleting the dependency entry.
-     */
     objAddr.objectId = newRefObjectId;

     newIsPinned = isObjectPinned(&objAddr, depRel);

-    /* Now search for dependency records */
+    if (oldIsPinned)
+    {
+        table_close(depRel, RowExclusiveLock);
+
+        /*
+         * If both are pinned, we need do nothing.  However, return 1 not 0,
+         * else callers will think this is an error case.
+         */
+        if (newIsPinned)
+            return 1;
+
+        /*
+         * There is no old dependency record, but we should insert a new one.
+         * Assume a normal dependency is wanted.
+         */
+        depAddr.classId = classId;
+        depAddr.objectId = objectId;
+        depAddr.objectSubId = 0;
+        recordDependencyOn(&depAddr, &objAddr, DEPENDENCY_NORMAL);
+
+        return 1;
+    }
+
+    /* There should be existing dependency record(s), so search. */
     ScanKeyInit(&key[0],
                 Anum_pg_depend_classid,
                 BTEqualStrategyNumber, F_OIDEQ,
diff --git a/src/backend/catalog/pg_proc.c b/src/backend/catalog/pg_proc.c
index 3a86f1e..557e0ea 100644
--- a/src/backend/catalog/pg_proc.c
+++ b/src/backend/catalog/pg_proc.c
@@ -88,6 +88,7 @@ ProcedureCreate(const char *procedureName,
                 List *parameterDefaults,
                 Datum trftypes,
                 Datum proconfig,
+                Oid prosupport,
                 float4 procost,
                 float4 prorows)
 {
@@ -319,7 +320,7 @@ ProcedureCreate(const char *procedureName,
     values[Anum_pg_proc_procost - 1] = Float4GetDatum(procost);
     values[Anum_pg_proc_prorows - 1] = Float4GetDatum(prorows);
     values[Anum_pg_proc_provariadic - 1] = ObjectIdGetDatum(variadicType);
-    values[Anum_pg_proc_prosupport - 1] = ObjectIdGetDatum(InvalidOid);
+    values[Anum_pg_proc_prosupport - 1] = ObjectIdGetDatum(prosupport);
     values[Anum_pg_proc_prokind - 1] = CharGetDatum(prokind);
     values[Anum_pg_proc_prosecdef - 1] = BoolGetDatum(security_definer);
     values[Anum_pg_proc_proleakproof - 1] = BoolGetDatum(isLeakProof);
@@ -656,6 +657,15 @@ ProcedureCreate(const char *procedureName,
         recordDependencyOnExpr(&myself, (Node *) parameterDefaults,
                                NIL, DEPENDENCY_NORMAL);

+    /* dependency on support function, if any */
+    if (OidIsValid(prosupport))
+    {
+        referenced.classId = ProcedureRelationId;
+        referenced.objectId = prosupport;
+        referenced.objectSubId = 0;
+        recordDependencyOn(&myself, &referenced, DEPENDENCY_NORMAL);
+    }
+
     /* dependency on owner */
     if (!is_update)
         recordDependencyOnOwner(ProcedureRelationId, retval, proowner);
diff --git a/src/backend/commands/functioncmds.c b/src/backend/commands/functioncmds.c
index eae2b09..4b13fdb 100644
--- a/src/backend/commands/functioncmds.c
+++ b/src/backend/commands/functioncmds.c
@@ -480,6 +480,7 @@ compute_common_attribute(ParseState *pstate,
                          List **set_items,
                          DefElem **cost_item,
                          DefElem **rows_item,
+                         DefElem **support_item,
                          DefElem **parallel_item)
 {
     if (strcmp(defel->defname, "volatility") == 0)
@@ -538,6 +539,15 @@ compute_common_attribute(ParseState *pstate,

         *rows_item = defel;
     }
+    else if (strcmp(defel->defname, "support") == 0)
+    {
+        if (is_procedure)
+            goto procedure_error;
+        if (*support_item)
+            goto duplicate_error;
+
+        *support_item = defel;
+    }
     else if (strcmp(defel->defname, "parallel") == 0)
     {
         if (is_procedure)
@@ -636,6 +646,45 @@ update_proconfig_value(ArrayType *a, List *set_items)
     return a;
 }

+static Oid
+interpret_func_support(DefElem *defel)
+{
+    List       *procName = defGetQualifiedName(defel);
+    Oid            procOid;
+    Oid            argList[1];
+
+    /*
+     * Support functions always take one INTERNAL argument and return
+     * INTERNAL.
+     */
+    argList[0] = INTERNALOID;
+
+    procOid = LookupFuncName(procName, 1, argList, true);
+    if (!OidIsValid(procOid))
+        ereport(ERROR,
+                (errcode(ERRCODE_UNDEFINED_FUNCTION),
+                 errmsg("function %s does not exist",
+                        func_signature_string(procName, 1, NIL, argList))));
+
+    if (get_func_rettype(procOid) != INTERNALOID)
+        ereport(ERROR,
+                (errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+                 errmsg("support function %s must return type %s",
+                        NameListToString(procName), "internal")));
+
+    /*
+     * Someday we might want an ACL check here; but for now, we insist that
+     * you be superuser to specify a support function, so privilege on the
+     * support function is moot.
+     */
+    if (!superuser())
+        ereport(ERROR,
+                (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+                 errmsg("must be superuser to specify a support function")));
+
+    return procOid;
+}
+

 /*
  * Dissect the list of options assembled in gram.y into function
@@ -656,6 +705,7 @@ compute_function_attributes(ParseState *pstate,
                             ArrayType **proconfig,
                             float4 *procost,
                             float4 *prorows,
+                            Oid *prosupport,
                             char *parallel_p)
 {
     ListCell   *option;
@@ -670,6 +720,7 @@ compute_function_attributes(ParseState *pstate,
     List       *set_items = NIL;
     DefElem    *cost_item = NULL;
     DefElem    *rows_item = NULL;
+    DefElem    *support_item = NULL;
     DefElem    *parallel_item = NULL;

     foreach(option, options)
@@ -727,6 +778,7 @@ compute_function_attributes(ParseState *pstate,
                                           &set_items,
                                           &cost_item,
                                           &rows_item,
+                                          &support_item,
                                           ¶llel_item))
         {
             /* recognized common option */
@@ -789,6 +841,8 @@ compute_function_attributes(ParseState *pstate,
                     (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
                      errmsg("ROWS must be positive")));
     }
+    if (support_item)
+        *prosupport = interpret_func_support(support_item);
     if (parallel_item)
         *parallel_p = interpret_func_parallel(parallel_item);
 }
@@ -894,6 +948,7 @@ CreateFunction(ParseState *pstate, CreateFunctionStmt *stmt)
     ArrayType  *proconfig;
     float4        procost;
     float4        prorows;
+    Oid            prosupport;
     HeapTuple    languageTuple;
     Form_pg_language languageStruct;
     List       *as_clause;
@@ -918,6 +973,7 @@ CreateFunction(ParseState *pstate, CreateFunctionStmt *stmt)
     proconfig = NULL;
     procost = -1;                /* indicates not set */
     prorows = -1;                /* indicates not set */
+    prosupport = InvalidOid;
     parallel = PROPARALLEL_UNSAFE;

     /* Extract non-default attributes from stmt->options list */
@@ -927,7 +983,8 @@ CreateFunction(ParseState *pstate, CreateFunctionStmt *stmt)
                                 &as_clause, &language, &transformDefElem,
                                 &isWindowFunc, &volatility,
                                 &isStrict, &security, &isLeakProof,
-                                &proconfig, &procost, &prorows, ¶llel);
+                                &proconfig, &procost, &prorows,
+                                &prosupport, ¶llel);

     /* Look up the language and validate permissions */
     languageTuple = SearchSysCache1(LANGNAME, PointerGetDatum(language));
@@ -1114,6 +1171,7 @@ CreateFunction(ParseState *pstate, CreateFunctionStmt *stmt)
                            parameterDefaults,
                            PointerGetDatum(trftypes),
                            PointerGetDatum(proconfig),
+                           prosupport,
                            procost,
                            prorows);
 }
@@ -1188,6 +1246,7 @@ AlterFunction(ParseState *pstate, AlterFunctionStmt *stmt)
     List       *set_items = NIL;
     DefElem    *cost_item = NULL;
     DefElem    *rows_item = NULL;
+    DefElem    *support_item = NULL;
     DefElem    *parallel_item = NULL;
     ObjectAddress address;

@@ -1195,6 +1254,8 @@ AlterFunction(ParseState *pstate, AlterFunctionStmt *stmt)

     funcOid = LookupFuncWithArgs(stmt->objtype, stmt->func, false);

+    ObjectAddressSet(address, ProcedureRelationId, funcOid);
+
     tup = SearchSysCacheCopy1(PROCOID, ObjectIdGetDatum(funcOid));
     if (!HeapTupleIsValid(tup)) /* should not happen */
         elog(ERROR, "cache lookup failed for function %u", funcOid);
@@ -1229,6 +1290,7 @@ AlterFunction(ParseState *pstate, AlterFunctionStmt *stmt)
                                      &set_items,
                                      &cost_item,
                                      &rows_item,
+                                     &support_item,
                                      ¶llel_item) == false)
             elog(ERROR, "option \"%s\" not recognized", defel->defname);
     }
@@ -1267,6 +1329,28 @@ AlterFunction(ParseState *pstate, AlterFunctionStmt *stmt)
                     (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
                      errmsg("ROWS is not applicable when function does not return a set")));
     }
+    if (support_item)
+    {
+        /* interpret_func_support handles the privilege check */
+        Oid            newsupport = interpret_func_support(support_item);
+
+        /* Add or replace dependency on support function */
+        if (OidIsValid(procForm->prosupport))
+            changeDependencyFor(ProcedureRelationId, funcOid,
+                                ProcedureRelationId, procForm->prosupport,
+                                newsupport);
+        else
+        {
+            ObjectAddress referenced;
+
+            referenced.classId = ProcedureRelationId;
+            referenced.objectId = newsupport;
+            referenced.objectSubId = 0;
+            recordDependencyOn(&address, &referenced, DEPENDENCY_NORMAL);
+        }
+
+        procForm->prosupport = newsupport;
+    }
     if (set_items)
     {
         Datum        datum;
@@ -1309,8 +1393,6 @@ AlterFunction(ParseState *pstate, AlterFunctionStmt *stmt)

     InvokeObjectPostAlterHook(ProcedureRelationId, funcOid, 0);

-    ObjectAddressSet(address, ProcedureRelationId, funcOid);
-
     table_close(rel, NoLock);
     heap_freetuple(tup);

diff --git a/src/backend/commands/proclang.c b/src/backend/commands/proclang.c
index c2e9e41..59c4e8d 100644
--- a/src/backend/commands/proclang.c
+++ b/src/backend/commands/proclang.c
@@ -141,6 +141,7 @@ CreateProceduralLanguage(CreatePLangStmt *stmt)
                                       NIL,
                                       PointerGetDatum(NULL),
                                       PointerGetDatum(NULL),
+                                      InvalidOid,
                                       1,
                                       0);
             handlerOid = tmpAddr.objectId;
@@ -180,6 +181,7 @@ CreateProceduralLanguage(CreatePLangStmt *stmt)
                                           NIL,
                                           PointerGetDatum(NULL),
                                           PointerGetDatum(NULL),
+                                          InvalidOid,
                                           1,
                                           0);
                 inlineOid = tmpAddr.objectId;
@@ -222,6 +224,7 @@ CreateProceduralLanguage(CreatePLangStmt *stmt)
                                           NIL,
                                           PointerGetDatum(NULL),
                                           PointerGetDatum(NULL),
+                                          InvalidOid,
                                           1,
                                           0);
                 valOid = tmpAddr.objectId;
diff --git a/src/backend/commands/typecmds.c b/src/backend/commands/typecmds.c
index 35a6485..b0a61a3 100644
--- a/src/backend/commands/typecmds.c
+++ b/src/backend/commands/typecmds.c
@@ -1664,6 +1664,7 @@ makeRangeConstructors(const char *name, Oid namespace,
                                  NIL,    /* parameterDefaults */
                                  PointerGetDatum(NULL), /* trftypes */
                                  PointerGetDatum(NULL), /* proconfig */
+                                 InvalidOid,    /* prosupport */
                                  1.0,    /* procost */
                                  0.0);    /* prorows */

diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index d8a3c2d..4c5f00d 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -677,7 +677,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
     SERIALIZABLE SERVER SESSION SESSION_USER SET SETS SETOF SHARE SHOW
     SIMILAR SIMPLE SKIP SMALLINT SNAPSHOT SOME SQL_P STABLE STANDALONE_P
     START STATEMENT STATISTICS STDIN STDOUT STORAGE STRICT_P STRIP_P
-    SUBSCRIPTION SUBSTRING SYMMETRIC SYSID SYSTEM_P
+    SUBSCRIPTION SUBSTRING SUPPORT SYMMETRIC SYSID SYSTEM_P

     TABLE TABLES TABLESAMPLE TABLESPACE TEMP TEMPLATE TEMPORARY TEXT_P THEN
     TIES TIME TIMESTAMP TO TRAILING TRANSACTION TRANSFORM
@@ -7888,6 +7888,10 @@ common_func_opt_item:
                 {
                     $$ = makeDefElem("rows", (Node *)$2, @1);
                 }
+            | SUPPORT any_name
+                {
+                    $$ = makeDefElem("support", (Node *)$2, @1);
+                }
             | FunctionSetResetClause
                 {
                     /* we abuse the normal content of a DefElem here */
@@ -15218,6 +15222,7 @@ unreserved_keyword:
             | STRICT_P
             | STRIP_P
             | SUBSCRIPTION
+            | SUPPORT
             | SYSID
             | SYSTEM_P
             | TABLES
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index 302df16..4bda11c 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -2638,6 +2638,21 @@ pg_get_functiondef(PG_FUNCTION_ARGS)
     if (proc->prorows > 0 && proc->prorows != 1000)
         appendStringInfo(&buf, " ROWS %g", proc->prorows);

+    if (proc->prosupport)
+    {
+        Oid            argtypes[1];
+
+        /*
+         * We should qualify the support function's name if it wouldn't be
+         * resolved by lookup in the current search path.
+         */
+        argtypes[0] = INTERNALOID;
+        appendStringInfo(&buf, " SUPPORT %s",
+                         generate_function_name(proc->prosupport, 1,
+                                                NIL, argtypes,
+                                                false, NULL, EXPR_KIND_NONE));
+    }
+
     if (oldlen != buf.len)
         appendStringInfoChar(&buf, '\n');

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 2b1a947..615d997 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -11466,6 +11466,7 @@ dumpFunc(Archive *fout, FuncInfo *finfo)
     char       *proconfig;
     char       *procost;
     char       *prorows;
+    char       *prosupport;
     char       *proparallel;
     char       *lanname;
     char       *rettypename;
@@ -11488,7 +11489,26 @@ dumpFunc(Archive *fout, FuncInfo *finfo)
     asPart = createPQExpBuffer();

     /* Fetch function-specific details */
-    if (fout->remoteVersion >= 110000)
+    if (fout->remoteVersion >= 120000)
+    {
+        /*
+         * prosupport was added in 12
+         */
+        appendPQExpBuffer(query,
+                          "SELECT proretset, prosrc, probin, "
+                          "pg_catalog.pg_get_function_arguments(oid) AS funcargs, "
+                          "pg_catalog.pg_get_function_identity_arguments(oid) AS funciargs, "
+                          "pg_catalog.pg_get_function_result(oid) AS funcresult, "
+                          "array_to_string(protrftypes, ' ') AS protrftypes, "
+                          "prokind, provolatile, proisstrict, prosecdef, "
+                          "proleakproof, proconfig, procost, prorows, "
+                          "prosupport, proparallel, "
+                          "(SELECT lanname FROM pg_catalog.pg_language WHERE oid = prolang) AS lanname "
+                          "FROM pg_catalog.pg_proc "
+                          "WHERE oid = '%u'::pg_catalog.oid",
+                          finfo->dobj.catId.oid);
+    }
+    else if (fout->remoteVersion >= 110000)
     {
         /*
          * prokind was added in 11
@@ -11501,7 +11521,7 @@ dumpFunc(Archive *fout, FuncInfo *finfo)
                           "array_to_string(protrftypes, ' ') AS protrftypes, "
                           "prokind, provolatile, proisstrict, prosecdef, "
                           "proleakproof, proconfig, procost, prorows, "
-                          "proparallel, "
+                          "'-' AS prosupport, proparallel, "
                           "(SELECT lanname FROM pg_catalog.pg_language WHERE oid = prolang) AS lanname "
                           "FROM pg_catalog.pg_proc "
                           "WHERE oid = '%u'::pg_catalog.oid",
@@ -11521,7 +11541,7 @@ dumpFunc(Archive *fout, FuncInfo *finfo)
                           "CASE WHEN proiswindow THEN 'w' ELSE 'f' END AS prokind, "
                           "provolatile, proisstrict, prosecdef, "
                           "proleakproof, proconfig, procost, prorows, "
-                          "proparallel, "
+                          "'-' AS prosupport, proparallel, "
                           "(SELECT lanname FROM pg_catalog.pg_language WHERE oid = prolang) AS lanname "
                           "FROM pg_catalog.pg_proc "
                           "WHERE oid = '%u'::pg_catalog.oid",
@@ -11541,6 +11561,7 @@ dumpFunc(Archive *fout, FuncInfo *finfo)
                           "CASE WHEN proiswindow THEN 'w' ELSE 'f' END AS prokind, "
                           "provolatile, proisstrict, prosecdef, "
                           "proleakproof, proconfig, procost, prorows, "
+                          "'-' AS prosupport, "
                           "(SELECT lanname FROM pg_catalog.pg_language WHERE oid = prolang) AS lanname "
                           "FROM pg_catalog.pg_proc "
                           "WHERE oid = '%u'::pg_catalog.oid",
@@ -11559,6 +11580,7 @@ dumpFunc(Archive *fout, FuncInfo *finfo)
                           "CASE WHEN proiswindow THEN 'w' ELSE 'f' END AS prokind, "
                           "provolatile, proisstrict, prosecdef, "
                           "proleakproof, proconfig, procost, prorows, "
+                          "'-' AS prosupport, "
                           "(SELECT lanname FROM pg_catalog.pg_language WHERE oid = prolang) AS lanname "
                           "FROM pg_catalog.pg_proc "
                           "WHERE oid = '%u'::pg_catalog.oid",
@@ -11579,6 +11601,7 @@ dumpFunc(Archive *fout, FuncInfo *finfo)
                           "provolatile, proisstrict, prosecdef, "
                           "false AS proleakproof, "
                           " proconfig, procost, prorows, "
+                          "'-' AS prosupport, "
                           "(SELECT lanname FROM pg_catalog.pg_language WHERE oid = prolang) AS lanname "
                           "FROM pg_catalog.pg_proc "
                           "WHERE oid = '%u'::pg_catalog.oid",
@@ -11593,6 +11616,7 @@ dumpFunc(Archive *fout, FuncInfo *finfo)
                           "provolatile, proisstrict, prosecdef, "
                           "false AS proleakproof, "
                           "proconfig, procost, prorows, "
+                          "'-' AS prosupport, "
                           "(SELECT lanname FROM pg_catalog.pg_language WHERE oid = prolang) AS lanname "
                           "FROM pg_catalog.pg_proc "
                           "WHERE oid = '%u'::pg_catalog.oid",
@@ -11607,6 +11631,7 @@ dumpFunc(Archive *fout, FuncInfo *finfo)
                           "provolatile, proisstrict, prosecdef, "
                           "false AS proleakproof, "
                           "null AS proconfig, 0 AS procost, 0 AS prorows, "
+                          "'-' AS prosupport, "
                           "(SELECT lanname FROM pg_catalog.pg_language WHERE oid = prolang) AS lanname "
                           "FROM pg_catalog.pg_proc "
                           "WHERE oid = '%u'::pg_catalog.oid",
@@ -11623,6 +11648,7 @@ dumpFunc(Archive *fout, FuncInfo *finfo)
                           "provolatile, proisstrict, prosecdef, "
                           "false AS proleakproof, "
                           "null AS proconfig, 0 AS procost, 0 AS prorows, "
+                          "'-' AS prosupport, "
                           "(SELECT lanname FROM pg_catalog.pg_language WHERE oid = prolang) AS lanname "
                           "FROM pg_catalog.pg_proc "
                           "WHERE oid = '%u'::pg_catalog.oid",
@@ -11660,6 +11686,7 @@ dumpFunc(Archive *fout, FuncInfo *finfo)
     proconfig = PQgetvalue(res, 0, PQfnumber(res, "proconfig"));
     procost = PQgetvalue(res, 0, PQfnumber(res, "procost"));
     prorows = PQgetvalue(res, 0, PQfnumber(res, "prorows"));
+    prosupport = PQgetvalue(res, 0, PQfnumber(res, "prosupport"));

     if (PQfnumber(res, "proparallel") != -1)
         proparallel = PQgetvalue(res, 0, PQfnumber(res, "proparallel"));
@@ -11873,6 +11900,12 @@ dumpFunc(Archive *fout, FuncInfo *finfo)
         strcmp(prorows, "0") != 0 && strcmp(prorows, "1000") != 0)
         appendPQExpBuffer(q, " ROWS %s", prorows);

+    if (strcmp(prosupport, "-") != 0)
+    {
+        /* We rely on regprocout to provide quoting and qualification */
+        appendPQExpBuffer(q, " SUPPORT %s", prosupport);
+    }
+
     if (proparallel != NULL && proparallel[0] != PROPARALLEL_UNSAFE)
     {
         if (proparallel[0] == PROPARALLEL_SAFE)
diff --git a/src/bin/pg_dump/t/002_pg_dump.pl b/src/bin/pg_dump/t/002_pg_dump.pl
index 4ff358a..d22ca73 100644
--- a/src/bin/pg_dump/t/002_pg_dump.pl
+++ b/src/bin/pg_dump/t/002_pg_dump.pl
@@ -1774,6 +1774,20 @@ my %tests = (
         unlike => { exclude_dump_test_schema => 1, },
     },

+    'CREATE FUNCTION ... SUPPORT' => {
+        create_order => 41,
+        create_sql =>
+          'CREATE FUNCTION dump_test.func_with_support() RETURNS int LANGUAGE sql AS $$ SELECT 1 $$ SUPPORT
varchar_support;',
+        regexp => qr/^
+            \QCREATE FUNCTION dump_test.func_with_support() RETURNS integer\E
+            \n\s+\QLANGUAGE sql SUPPORT varchar_support\E
+            \n\s+AS\ \$\$\Q SELECT 1 \E\$\$;
+            /xm,
+        like =>
+          { %full_runs, %dump_test_schema_runs, section_pre_data => 1, },
+        unlike => { exclude_dump_test_schema => 1, },
+    },
+
     'CREATE PROCEDURE dump_test.ptest1' => {
         create_order => 41,
         create_sql   => 'CREATE PROCEDURE dump_test.ptest1(a int)
diff --git a/src/include/catalog/pg_proc.h b/src/include/catalog/pg_proc.h
index b433769..e5270d2 100644
--- a/src/include/catalog/pg_proc.h
+++ b/src/include/catalog/pg_proc.h
@@ -201,6 +201,7 @@ extern ObjectAddress ProcedureCreate(const char *procedureName,
                 List *parameterDefaults,
                 Datum trftypes,
                 Datum proconfig,
+                Oid prosupport,
                 float4 procost,
                 float4 prorows);

diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h
index adeb834..f054440 100644
--- a/src/include/parser/kwlist.h
+++ b/src/include/parser/kwlist.h
@@ -387,6 +387,7 @@ PG_KEYWORD("strict", STRICT_P, UNRESERVED_KEYWORD)
 PG_KEYWORD("strip", STRIP_P, UNRESERVED_KEYWORD)
 PG_KEYWORD("subscription", SUBSCRIPTION, UNRESERVED_KEYWORD)
 PG_KEYWORD("substring", SUBSTRING, COL_NAME_KEYWORD)
+PG_KEYWORD("support", SUPPORT, UNRESERVED_KEYWORD)
 PG_KEYWORD("symmetric", SYMMETRIC, RESERVED_KEYWORD)
 PG_KEYWORD("sysid", SYSID, UNRESERVED_KEYWORD)
 PG_KEYWORD("system", SYSTEM_P, UNRESERVED_KEYWORD)
diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out
index 7bb8ca9..4db792c 100644
--- a/src/test/regress/expected/alter_table.out
+++ b/src/test/regress/expected/alter_table.out
@@ -3050,10 +3050,9 @@ DETAIL:  System catalog modifications are currently disallowed.
 -- instead create in public first, move to catalog
 CREATE TABLE new_system_table(id serial primary key, othercol text);
 ALTER TABLE new_system_table SET SCHEMA pg_catalog;
--- XXX: it's currently impossible to move relations out of pg_catalog
 ALTER TABLE new_system_table SET SCHEMA public;
-ERROR:  cannot remove dependency on schema pg_catalog because it is a system object
--- move back, will be ignored -- already there
+ALTER TABLE new_system_table SET SCHEMA pg_catalog;
+-- will be ignored -- already there:
 ALTER TABLE new_system_table SET SCHEMA pg_catalog;
 ALTER TABLE new_system_table RENAME TO old_system_table;
 CREATE INDEX old_system_table__othercol ON old_system_table (othercol);
diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql
index a498e4e..d806430 100644
--- a/src/test/regress/sql/alter_table.sql
+++ b/src/test/regress/sql/alter_table.sql
@@ -1896,10 +1896,9 @@ CREATE TABLE pg_catalog.new_system_table();
 -- instead create in public first, move to catalog
 CREATE TABLE new_system_table(id serial primary key, othercol text);
 ALTER TABLE new_system_table SET SCHEMA pg_catalog;
-
--- XXX: it's currently impossible to move relations out of pg_catalog
 ALTER TABLE new_system_table SET SCHEMA public;
--- move back, will be ignored -- already there
+ALTER TABLE new_system_table SET SCHEMA pg_catalog;
+-- will be ignored -- already there:
 ALTER TABLE new_system_table SET SCHEMA pg_catalog;
 ALTER TABLE new_system_table RENAME TO old_system_table;
 CREATE INDEX old_system_table__othercol ON old_system_table (othercol);
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index d85a83a..3a906c7 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -2756,6 +2756,7 @@ estimate_path_cost_size(PlannerInfo *root,
             startup_cost = ofpinfo->rel_startup_cost;
             startup_cost += aggcosts.transCost.startup;
             startup_cost += aggcosts.transCost.per_tuple * input_rows;
+            startup_cost += aggcosts.finalCost.startup;
             startup_cost += (cpu_operator_cost * numGroupCols) * input_rows;
             startup_cost += ptarget->cost.startup;

@@ -2767,7 +2768,7 @@ estimate_path_cost_size(PlannerInfo *root,
              *-----
              */
             run_cost = ofpinfo->rel_total_cost - ofpinfo->rel_startup_cost;
-            run_cost += aggcosts.finalCost * numGroups;
+            run_cost += aggcosts.finalCost.per_tuple * numGroups;
             run_cost += cpu_tuple_cost * numGroups;
             run_cost += ptarget->cost.per_tuple * numGroups;

diff --git a/doc/src/sgml/xfunc.sgml b/doc/src/sgml/xfunc.sgml
index d70aa6e..b486ef3 100644
--- a/doc/src/sgml/xfunc.sgml
+++ b/doc/src/sgml/xfunc.sgml
@@ -3439,4 +3439,25 @@ supportfn(internal) returns internal
     simplify.  Ensure rigorous equivalence between the simplified
     expression and an actual execution of the target function.
    </para>
+
+   <para>
+    For target functions that return boolean, it is often useful to estimate
+    the fraction of rows that will be selected by a WHERE clause using that
+    function.  This can be done by a support function that implements
+    the <literal>SupportRequestSelectivity</literal> request type.
+   </para>
+
+   <para>
+    If the target function's runtime is highly dependent on its inputs,
+    it may be useful to provide a non-constant cost estimate for it.
+    This can be done by a support function that implements
+    the <literal>SupportRequestCost</literal> request type.
+   </para>
+
+   <para>
+    For target functions that return sets, it is often useful to provide
+    a non-constant estimate for the number of rows that will be returned.
+    This can be done by a support function that implements
+    the <literal>SupportRequestRows</literal> request type.
+   </para>
   </sect1>
diff --git a/src/backend/optimizer/path/clausesel.c b/src/backend/optimizer/path/clausesel.c
index 3739b98..9e3f708 100644
--- a/src/backend/optimizer/path/clausesel.c
+++ b/src/backend/optimizer/path/clausesel.c
@@ -760,6 +760,21 @@ clause_selectivity(PlannerInfo *root,
         if (IsA(clause, DistinctExpr))
             s1 = 1.0 - s1;
     }
+    else if (is_funcclause(clause))
+    {
+        FuncExpr   *funcclause = (FuncExpr *) clause;
+
+        /* Try to get an estimate from the support function, if any */
+        s1 = function_selectivity(root,
+                                  funcclause->funcid,
+                                  funcclause->args,
+                                  funcclause->inputcollid,
+                                  treat_as_join_clause(clause, rinfo,
+                                                       varRelid, sjinfo),
+                                  varRelid,
+                                  jointype,
+                                  sjinfo);
+    }
     else if (IsA(clause, ScalarArrayOpExpr))
     {
         /* Use node specific selectivity calculation function */
diff --git a/src/backend/optimizer/path/costsize.c b/src/backend/optimizer/path/costsize.c
index 99c5ad9..f4f1c95 100644
--- a/src/backend/optimizer/path/costsize.c
+++ b/src/backend/optimizer/path/costsize.c
@@ -2080,9 +2080,9 @@ cost_agg(Path *path, PlannerInfo *root,
     /*
      * The transCost.per_tuple component of aggcosts should be charged once
      * per input tuple, corresponding to the costs of evaluating the aggregate
-     * transfns and their input expressions (with any startup cost of course
-     * charged but once).  The finalCost component is charged once per output
-     * tuple, corresponding to the costs of evaluating the finalfns.
+     * transfns and their input expressions. The finalCost.per_tuple component
+     * is charged once per output tuple, corresponding to the costs of
+     * evaluating the finalfns.  Startup costs are of course charged but once.
      *
      * If we are grouping, we charge an additional cpu_operator_cost per
      * grouping column per input tuple for grouping comparisons.
@@ -2104,7 +2104,8 @@ cost_agg(Path *path, PlannerInfo *root,
         startup_cost = input_total_cost;
         startup_cost += aggcosts->transCost.startup;
         startup_cost += aggcosts->transCost.per_tuple * input_tuples;
-        startup_cost += aggcosts->finalCost;
+        startup_cost += aggcosts->finalCost.startup;
+        startup_cost += aggcosts->finalCost.per_tuple;
         /* we aren't grouping */
         total_cost = startup_cost + cpu_tuple_cost;
         output_tuples = 1;
@@ -2123,7 +2124,8 @@ cost_agg(Path *path, PlannerInfo *root,
         total_cost += aggcosts->transCost.startup;
         total_cost += aggcosts->transCost.per_tuple * input_tuples;
         total_cost += (cpu_operator_cost * numGroupCols) * input_tuples;
-        total_cost += aggcosts->finalCost * numGroups;
+        total_cost += aggcosts->finalCost.startup;
+        total_cost += aggcosts->finalCost.per_tuple * numGroups;
         total_cost += cpu_tuple_cost * numGroups;
         output_tuples = numGroups;
     }
@@ -2136,8 +2138,9 @@ cost_agg(Path *path, PlannerInfo *root,
         startup_cost += aggcosts->transCost.startup;
         startup_cost += aggcosts->transCost.per_tuple * input_tuples;
         startup_cost += (cpu_operator_cost * numGroupCols) * input_tuples;
+        startup_cost += aggcosts->finalCost.startup;
         total_cost = startup_cost;
-        total_cost += aggcosts->finalCost * numGroups;
+        total_cost += aggcosts->finalCost.per_tuple * numGroups;
         total_cost += cpu_tuple_cost * numGroups;
         output_tuples = numGroups;
     }
@@ -2202,7 +2205,11 @@ cost_windowagg(Path *path, PlannerInfo *root,
         Cost        wfunccost;
         QualCost    argcosts;

-        wfunccost = get_func_cost(wfunc->winfnoid) * cpu_operator_cost;
+        argcosts.startup = argcosts.per_tuple = 0;
+        add_function_cost(root, wfunc->winfnoid, (Node *) wfunc,
+                          &argcosts);
+        startup_cost += argcosts.startup;
+        wfunccost = argcosts.per_tuple;

         /* also add the input expressions' cost to per-input-row costs */
         cost_qual_eval_node(&argcosts, (Node *) wfunc->args, root);
@@ -3832,8 +3839,8 @@ cost_qual_eval_walker(Node *node, cost_qual_eval_context *context)
      */
     if (IsA(node, FuncExpr))
     {
-        context->total.per_tuple +=
-            get_func_cost(((FuncExpr *) node)->funcid) * cpu_operator_cost;
+        add_function_cost(context->root, ((FuncExpr *) node)->funcid, node,
+                          &context->total);
     }
     else if (IsA(node, OpExpr) ||
              IsA(node, DistinctExpr) ||
@@ -3841,8 +3848,8 @@ cost_qual_eval_walker(Node *node, cost_qual_eval_context *context)
     {
         /* rely on struct equivalence to treat these all alike */
         set_opfuncid((OpExpr *) node);
-        context->total.per_tuple +=
-            get_func_cost(((OpExpr *) node)->opfuncid) * cpu_operator_cost;
+        add_function_cost(context->root, ((OpExpr *) node)->opfuncid, node,
+                          &context->total);
     }
     else if (IsA(node, ScalarArrayOpExpr))
     {
@@ -3852,10 +3859,15 @@ cost_qual_eval_walker(Node *node, cost_qual_eval_context *context)
          */
         ScalarArrayOpExpr *saop = (ScalarArrayOpExpr *) node;
         Node       *arraynode = (Node *) lsecond(saop->args);
+        QualCost    sacosts;

         set_sa_opfuncid(saop);
-        context->total.per_tuple += get_func_cost(saop->opfuncid) *
-            cpu_operator_cost * estimate_array_length(arraynode) * 0.5;
+        sacosts.startup = sacosts.per_tuple = 0;
+        add_function_cost(context->root, saop->opfuncid, NULL,
+                          &sacosts);
+        context->total.startup += sacosts.startup;
+        context->total.per_tuple += sacosts.per_tuple *
+            estimate_array_length(arraynode) * 0.5;
     }
     else if (IsA(node, Aggref) ||
              IsA(node, WindowFunc))
@@ -3881,11 +3893,13 @@ cost_qual_eval_walker(Node *node, cost_qual_eval_context *context)
         /* check the result type's input function */
         getTypeInputInfo(iocoerce->resulttype,
                          &iofunc, &typioparam);
-        context->total.per_tuple += get_func_cost(iofunc) * cpu_operator_cost;
+        add_function_cost(context->root, iofunc, NULL,
+                          &context->total);
         /* check the input type's output function */
         getTypeOutputInfo(exprType((Node *) iocoerce->arg),
                           &iofunc, &typisvarlena);
-        context->total.per_tuple += get_func_cost(iofunc) * cpu_operator_cost;
+        add_function_cost(context->root, iofunc, NULL,
+                          &context->total);
     }
     else if (IsA(node, ArrayCoerceExpr))
     {
@@ -3909,8 +3923,8 @@ cost_qual_eval_walker(Node *node, cost_qual_eval_context *context)
         {
             Oid            opid = lfirst_oid(lc);

-            context->total.per_tuple += get_func_cost(get_opcode(opid)) *
-                cpu_operator_cost;
+            add_function_cost(context->root, get_opcode(opid), NULL,
+                              &context->total);
         }
     }
     else if (IsA(node, MinMaxExpr) ||
@@ -4910,7 +4924,7 @@ set_function_size_estimates(PlannerInfo *root, RelOptInfo *rel)
     foreach(lc, rte->functions)
     {
         RangeTblFunction *rtfunc = (RangeTblFunction *) lfirst(lc);
-        double        ntup = expression_returns_set_rows(rtfunc->funcexpr);
+        double        ntup = expression_returns_set_rows(root, rtfunc->funcexpr);

         if (ntup > rel->tuples)
             rel->tuples = ntup;
diff --git a/src/backend/optimizer/util/clauses.c b/src/backend/optimizer/util/clauses.c
index 061a855..93eddd3 100644
--- a/src/backend/optimizer/util/clauses.c
+++ b/src/backend/optimizer/util/clauses.c
@@ -35,6 +35,7 @@
 #include "nodes/supportnodes.h"
 #include "optimizer/clauses.h"
 #include "optimizer/cost.h"
+#include "optimizer/plancat.h"
 #include "optimizer/planmain.h"
 #include "optimizer/prep.h"
 #include "optimizer/var.h"
@@ -583,19 +584,24 @@ get_agg_clause_costs_walker(Node *node, get_agg_clause_costs_context *context)
         if (DO_AGGSPLIT_COMBINE(context->aggsplit))
         {
             /* charge for combining previously aggregated states */
-            costs->transCost.per_tuple += get_func_cost(aggcombinefn) * cpu_operator_cost;
+            add_function_cost(context->root, aggcombinefn, NULL,
+                              &costs->transCost);
         }
         else
-            costs->transCost.per_tuple += get_func_cost(aggtransfn) * cpu_operator_cost;
+            add_function_cost(context->root, aggtransfn, NULL,
+                              &costs->transCost);
         if (DO_AGGSPLIT_DESERIALIZE(context->aggsplit) &&
             OidIsValid(aggdeserialfn))
-            costs->transCost.per_tuple += get_func_cost(aggdeserialfn) * cpu_operator_cost;
+            add_function_cost(context->root, aggdeserialfn, NULL,
+                              &costs->transCost);
         if (DO_AGGSPLIT_SERIALIZE(context->aggsplit) &&
             OidIsValid(aggserialfn))
-            costs->finalCost += get_func_cost(aggserialfn) * cpu_operator_cost;
+            add_function_cost(context->root, aggserialfn, NULL,
+                              &costs->finalCost);
         if (!DO_AGGSPLIT_SKIPFINAL(context->aggsplit) &&
             OidIsValid(aggfinalfn))
-            costs->finalCost += get_func_cost(aggfinalfn) * cpu_operator_cost;
+            add_function_cost(context->root, aggfinalfn, NULL,
+                              &costs->finalCost);

         /*
          * These costs are incurred only by the initial aggregate node, so we
@@ -632,8 +638,8 @@ get_agg_clause_costs_walker(Node *node, get_agg_clause_costs_context *context)
         {
             cost_qual_eval_node(&argcosts, (Node *) aggref->aggdirectargs,
                                 context->root);
-            costs->transCost.startup += argcosts.startup;
-            costs->finalCost += argcosts.per_tuple;
+            costs->finalCost.startup += argcosts.startup;
+            costs->finalCost.per_tuple += argcosts.per_tuple;
         }

         /*
@@ -801,7 +807,7 @@ find_window_functions_walker(Node *node, WindowFuncLists *lists)
  * Note: keep this in sync with expression_returns_set() in nodes/nodeFuncs.c.
  */
 double
-expression_returns_set_rows(Node *clause)
+expression_returns_set_rows(PlannerInfo *root, Node *clause)
 {
     if (clause == NULL)
         return 1.0;
@@ -810,7 +816,7 @@ expression_returns_set_rows(Node *clause)
         FuncExpr   *expr = (FuncExpr *) clause;

         if (expr->funcretset)
-            return clamp_row_est(get_func_rows(expr->funcid));
+            return clamp_row_est(get_function_rows(root, expr->funcid, clause));
     }
     if (IsA(clause, OpExpr))
     {
@@ -819,7 +825,7 @@ expression_returns_set_rows(Node *clause)
         if (expr->opretset)
         {
             set_opfuncid(expr);
-            return clamp_row_est(get_func_rows(expr->opfuncid));
+            return clamp_row_est(get_function_rows(root, expr->opfuncid, clause));
         }
     }
     return 1.0;
diff --git a/src/backend/optimizer/util/pathnode.c b/src/backend/optimizer/util/pathnode.c
index b2637d0..999571a 100644
--- a/src/backend/optimizer/util/pathnode.c
+++ b/src/backend/optimizer/util/pathnode.c
@@ -2596,7 +2596,7 @@ create_set_projection_path(PlannerInfo *root,
         Node       *node = (Node *) lfirst(lc);
         double        itemrows;

-        itemrows = expression_returns_set_rows(node);
+        itemrows = expression_returns_set_rows(root, node);
         if (tlist_rows < itemrows)
             tlist_rows = itemrows;
     }
diff --git a/src/backend/optimizer/util/plancat.c b/src/backend/optimizer/util/plancat.c
index 261492e..f2250eb 100644
--- a/src/backend/optimizer/util/plancat.c
+++ b/src/backend/optimizer/util/plancat.c
@@ -29,10 +29,12 @@
 #include "catalog/heap.h"
 #include "catalog/partition.h"
 #include "catalog/pg_am.h"
+#include "catalog/pg_proc.h"
 #include "catalog/pg_statistic_ext.h"
 #include "foreign/fdwapi.h"
 #include "miscadmin.h"
 #include "nodes/makefuncs.h"
+#include "nodes/supportnodes.h"
 #include "optimizer/clauses.h"
 #include "optimizer/cost.h"
 #include "optimizer/plancat.h"
@@ -1771,6 +1773,8 @@ restriction_selectivity(PlannerInfo *root,
  * Returns the selectivity of a specified join operator clause.
  * This code executes registered procedures stored in the
  * operator relation, by calling the function manager.
+ *
+ * See clause_selectivity() for the meaning of the additional parameters.
  */
 Selectivity
 join_selectivity(PlannerInfo *root,
@@ -1805,6 +1809,184 @@ join_selectivity(PlannerInfo *root,
 }

 /*
+ * function_selectivity
+ *
+ * Returns the selectivity of a specified boolean function clause.
+ * This code executes registered procedures stored in the
+ * pg_proc relation, by calling the function manager.
+ *
+ * See clause_selectivity() for the meaning of the additional parameters.
+ */
+Selectivity
+function_selectivity(PlannerInfo *root,
+                     Oid funcid,
+                     List *args,
+                     Oid inputcollid,
+                     bool is_join,
+                     int varRelid,
+                     JoinType jointype,
+                     SpecialJoinInfo *sjinfo)
+{
+    RegProcedure prosupport = get_func_support(funcid);
+    SupportRequestSelectivity req;
+    SupportRequestSelectivity *sresult;
+
+    /*
+     * If no support function is provided, use our historical default
+     * estimate, 0.3333333.  This seems a pretty unprincipled choice, but
+     * Postgres has been using that estimate for function calls since 1992.
+     * The hoariness of this behavior suggests that we should not be in too
+     * much hurry to use another value.
+     */
+    if (!prosupport)
+        return (Selectivity) 0.3333333;
+
+    req.type = T_SupportRequestSelectivity;
+    req.root = root;
+    req.funcid = funcid;
+    req.args = args;
+    req.inputcollid = inputcollid;
+    req.is_join = is_join;
+    req.varRelid = varRelid;
+    req.jointype = jointype;
+    req.sjinfo = sjinfo;
+    req.selectivity = -1;        /* to catch failure to set the value */
+
+    sresult = (SupportRequestSelectivity *)
+        DatumGetPointer(OidFunctionCall1(prosupport,
+                                         PointerGetDatum(&req)));
+
+    /* If support function fails, use default */
+    if (sresult != &req)
+        return (Selectivity) 0.3333333;
+
+    if (req.selectivity < 0.0 || req.selectivity > 1.0)
+        elog(ERROR, "invalid function selectivity: %f", req.selectivity);
+
+    return (Selectivity) req.selectivity;
+}
+
+/*
+ * add_function_cost
+ *
+ * Get an estimate of the execution cost of a function, and *add* it to
+ * the contents of *cost.  The estimate may include both one-time and
+ * per-tuple components, since QualCost does.
+ *
+ * The funcid must always be supplied.  If it is being called as the
+ * implementation of a specific parsetree node (FuncExpr, OpExpr,
+ * WindowFunc, etc), pass that as "node", else pass NULL.
+ *
+ * In some usages root might be NULL, too.
+ */
+void
+add_function_cost(PlannerInfo *root, Oid funcid, Node *node,
+                  QualCost *cost)
+{
+    HeapTuple    proctup;
+    Form_pg_proc procform;
+
+    proctup = SearchSysCache1(PROCOID, ObjectIdGetDatum(funcid));
+    if (!HeapTupleIsValid(proctup))
+        elog(ERROR, "cache lookup failed for function %u", funcid);
+    procform = (Form_pg_proc) GETSTRUCT(proctup);
+
+    if (OidIsValid(procform->prosupport))
+    {
+        SupportRequestCost req;
+        SupportRequestCost *sresult;
+
+        req.type = T_SupportRequestCost;
+        req.root = root;
+        req.funcid = funcid;
+        req.node = node;
+
+        /* Initialize cost fields so that support function doesn't have to */
+        req.startup = 0;
+        req.per_tuple = 0;
+
+        sresult = (SupportRequestCost *)
+            DatumGetPointer(OidFunctionCall1(procform->prosupport,
+                                             PointerGetDatum(&req)));
+
+        if (sresult == &req)
+        {
+            /* Success, so accumulate support function's estimate into *cost */
+            cost->startup += req.startup;
+            cost->per_tuple += req.per_tuple;
+            ReleaseSysCache(proctup);
+            return;
+        }
+    }
+
+    /* No support function, or it failed, so rely on procost */
+    cost->per_tuple += procform->procost * cpu_operator_cost;
+
+    ReleaseSysCache(proctup);
+}
+
+/*
+ * get_function_rows
+ *
+ * Get an estimate of the number of rows returned by a set-returning function.
+ *
+ * The funcid must always be supplied.  In current usage, the calling node
+ * will always be supplied, and will be either a FuncExpr or OpExpr.
+ * But it's a good idea to not fail if it's NULL.
+ *
+ * In some usages root might be NULL, too.
+ *
+ * Note: this returns the unfiltered result of the support function, if any.
+ * It's usually a good idea to apply clamp_row_est() to the result, but we
+ * leave it to the caller to do so.
+ */
+double
+get_function_rows(PlannerInfo *root, Oid funcid, Node *node)
+{
+    HeapTuple    proctup;
+    Form_pg_proc procform;
+    double        result;
+
+    proctup = SearchSysCache1(PROCOID, ObjectIdGetDatum(funcid));
+    if (!HeapTupleIsValid(proctup))
+        elog(ERROR, "cache lookup failed for function %u", funcid);
+    procform = (Form_pg_proc) GETSTRUCT(proctup);
+
+    Assert(procform->proretset);    /* else caller error */
+
+    if (OidIsValid(procform->prosupport))
+    {
+        SupportRequestRows req;
+        SupportRequestRows *sresult;
+
+        req.type = T_SupportRequestRows;
+        req.root = root;
+        req.funcid = funcid;
+        req.node = node;
+
+        req.rows = 0;            /* just for sanity */
+
+        sresult = (SupportRequestRows *)
+            DatumGetPointer(OidFunctionCall1(procform->prosupport,
+                                             PointerGetDatum(&req)));
+
+        if (sresult == &req)
+        {
+            /* Success */
+            ReleaseSysCache(proctup);
+            return req.rows;
+        }
+    }
+
+    /* No support function, or it failed, so rely on prorows */
+    result = procform->prorows;
+
+    ReleaseSysCache(proctup);
+
+    return result;
+}
+
+/*
  * has_unique_index
  *
  * Detect whether there is a unique index on the specified attribute
diff --git a/src/backend/utils/adt/selfuncs.c b/src/backend/utils/adt/selfuncs.c
index dcb35d8..7b15581 100644
--- a/src/backend/utils/adt/selfuncs.c
+++ b/src/backend/utils/adt/selfuncs.c
@@ -1576,17 +1576,6 @@ boolvarsel(PlannerInfo *root, Node *arg, int varRelid)
         selec = var_eq_const(&vardata, BooleanEqualOperator,
                              BoolGetDatum(true), false, true, false);
     }
-    else if (is_funcclause(arg))
-    {
-        /*
-         * If we have no stats and it's a function call, estimate 0.3333333.
-         * This seems a pretty unprincipled choice, but Postgres has been
-         * using that estimate for function calls since 1992.  The hoariness
-         * of this behavior suggests that we should not be in too much hurry
-         * to use another value.
-         */
-        selec = 0.3333333;
-    }
     else
     {
         /* Otherwise, the default estimate is 0.5 */
@@ -3492,7 +3481,7 @@ estimate_num_groups(PlannerInfo *root, List *groupExprs, double input_rows,
          * pointless to worry too much about this without much better
          * estimates for SRF output rowcounts than we have today.)
          */
-        this_srf_multiplier = expression_returns_set_rows(groupexpr);
+        this_srf_multiplier = expression_returns_set_rows(root, groupexpr);
         if (srf_multiplier < this_srf_multiplier)
             srf_multiplier = this_srf_multiplier;

diff --git a/src/backend/utils/cache/lsyscache.c b/src/backend/utils/cache/lsyscache.c
index fba0ee8..e88c45d 100644
--- a/src/backend/utils/cache/lsyscache.c
+++ b/src/backend/utils/cache/lsyscache.c
@@ -1605,41 +1605,28 @@ get_func_leakproof(Oid funcid)
 }

 /*
- * get_func_cost
- *        Given procedure id, return the function's procost field.
- */
-float4
-get_func_cost(Oid funcid)
-{
-    HeapTuple    tp;
-    float4        result;
-
-    tp = SearchSysCache1(PROCOID, ObjectIdGetDatum(funcid));
-    if (!HeapTupleIsValid(tp))
-        elog(ERROR, "cache lookup failed for function %u", funcid);
-
-    result = ((Form_pg_proc) GETSTRUCT(tp))->procost;
-    ReleaseSysCache(tp);
-    return result;
-}
-
-/*
- * get_func_rows
- *        Given procedure id, return the function's prorows field.
+ * get_func_support
+ *
+ *        Returns the support function OID associated with a given function,
+ *        or InvalidOid if there is none.
  */
-float4
-get_func_rows(Oid funcid)
+RegProcedure
+get_func_support(Oid funcid)
 {
     HeapTuple    tp;
-    float4        result;

     tp = SearchSysCache1(PROCOID, ObjectIdGetDatum(funcid));
-    if (!HeapTupleIsValid(tp))
-        elog(ERROR, "cache lookup failed for function %u", funcid);
+    if (HeapTupleIsValid(tp))
+    {
+        Form_pg_proc functup = (Form_pg_proc) GETSTRUCT(tp);
+        RegProcedure result;

-    result = ((Form_pg_proc) GETSTRUCT(tp))->prorows;
-    ReleaseSysCache(tp);
-    return result;
+        result = functup->prosupport;
+        ReleaseSysCache(tp);
+        return result;
+    }
+    else
+        return (RegProcedure) InvalidOid;
 }

 /*                ---------- RELATION CACHE ----------                     */
diff --git a/src/include/nodes/nodes.h b/src/include/nodes/nodes.h
index e029b40..e33b2ee 100644
--- a/src/include/nodes/nodes.h
+++ b/src/include/nodes/nodes.h
@@ -506,7 +506,10 @@ typedef enum NodeTag
     T_TsmRoutine,                /* in access/tsmapi.h */
     T_ForeignKeyCacheInfo,        /* in utils/rel.h */
     T_CallContext,                /* in nodes/parsenodes.h */
-    T_SupportRequestSimplify    /* in nodes/supportnodes.h */
+    T_SupportRequestSimplify,    /* in nodes/supportnodes.h */
+    T_SupportRequestSelectivity,    /* in nodes/supportnodes.h */
+    T_SupportRequestCost,        /* in nodes/supportnodes.h */
+    T_SupportRequestRows        /* in nodes/supportnodes.h */
 } NodeTag;

 /*
diff --git a/src/include/nodes/relation.h b/src/include/nodes/relation.h
index 3430061..af046f5 100644
--- a/src/include/nodes/relation.h
+++ b/src/include/nodes/relation.h
@@ -61,7 +61,7 @@ typedef struct AggClauseCosts
     bool        hasNonPartial;    /* does any agg not support partial mode? */
     bool        hasNonSerial;    /* is any partial agg non-serializable? */
     QualCost    transCost;        /* total per-input-row execution costs */
-    Cost        finalCost;        /* total per-aggregated-row costs */
+    QualCost    finalCost;        /* total per-aggregated-row costs */
     Size        transitionSpace;    /* space for pass-by-ref transition data */
 } AggClauseCosts;

diff --git a/src/include/nodes/supportnodes.h b/src/include/nodes/supportnodes.h
index 1f7d02b..1a3a36b 100644
--- a/src/include/nodes/supportnodes.h
+++ b/src/include/nodes/supportnodes.h
@@ -36,6 +36,7 @@
 #include "nodes/primnodes.h"

 struct PlannerInfo;                /* avoid including relation.h here */
+struct SpecialJoinInfo;


 /*
@@ -67,4 +68,103 @@ typedef struct SupportRequestSimplify
     FuncExpr   *fcall;            /* Function call to be simplified */
 } SupportRequestSimplify;

+/*
+ * The Selectivity request allows the support function to provide a
+ * selectivity estimate for a function appearing at top level of a WHERE
+ * clause (so it applies only to functions returning boolean).
+ *
+ * The input arguments are the same as are supplied to operator restriction
+ * and join estimators, except that we unify those two APIs into just one
+ * request type.  See clause_selectivity() for the details.
+ *
+ * If an estimate can be made, store it into the "selectivity" field and
+ * return the address of the SupportRequestSelectivity node; the estimate
+ * must be between 0 and 1 inclusive.  Return NULL if no estimate can be
+ * made (in which case the planner will fall back to a default estimate,
+ * traditionally 1/3).
+ *
+ * If the target function is being used as the implementation of an operator,
+ * the support function will not be used for this purpose; the operator's
+ * restriction or join estimator is consulted instead.
+ */
+typedef struct SupportRequestSelectivity
+{
+    NodeTag        type;
+
+    /* Input fields: */
+    struct PlannerInfo *root;    /* Planner's infrastructure */
+    Oid            funcid;            /* function we are inquiring about */
+    List       *args;            /* pre-simplified arguments to function */
+    Oid            inputcollid;    /* function's input collation */
+    bool        is_join;        /* is this a join or restriction case? */
+    int            varRelid;        /* if restriction, RTI of target relation */
+    JoinType    jointype;        /* if join, outer join type */
+    struct SpecialJoinInfo *sjinfo; /* if outer join, info about join */
+
+    /* Output fields: */
+    Selectivity selectivity;    /* returned selectivity estimate */
+} SupportRequestSelectivity;
+
+/*
+ * The Cost request allows the support function to provide an execution
+ * cost estimate for its target function.  The cost estimate can include
+ * both a one-time (query startup) component and a per-execution component.
+ * The estimate should *not* include the costs of evaluating the target
+ * function's arguments, only the target function itself.
+ *
+ * The "node" argument is normally the parse node that is invoking the
+ * target function.  This is a FuncExpr in the simplest case, but it could
+ * also be an OpExpr, DistinctExpr, NullIfExpr, or WindowFunc, or possibly
+ * other cases in future.  NULL is passed if the function cannot presume
+ * its arguments to be equivalent to what the calling node presents as
+ * arguments; that happens for, e.g., aggregate support functions and
+ * per-column comparison operators used by RowExprs.
+ *
+ * If an estimate can be made, store it into the cost fields and return the
+ * address of the SupportRequestCost node.  Return NULL if no estimate can be
+ * made, in which case the planner will rely on the target function's procost
+ * field.  (Note: while procost is automatically scaled by cpu_operator_cost,
+ * this is not the case for the outputs of the Cost request; the support
+ * function must scale its results appropriately on its own.)
+ */
+typedef struct SupportRequestCost
+{
+    NodeTag        type;
+
+    /* Input fields: */
+    struct PlannerInfo *root;    /* Planner's infrastructure (could be NULL) */
+    Oid            funcid;            /* function we are inquiring about */
+    Node       *node;            /* parse node invoking function, or NULL */
+
+    /* Output fields: */
+    Cost        startup;        /* one-time cost */
+    Cost        per_tuple;        /* per-evaluation cost */
+} SupportRequestCost;
+
+/*
+ * The Rows request allows the support function to provide an output rowcount
+ * estimate for its target function (so it applies only to set-returning
+ * functions).
+ *
+ * The "node" argument is the parse node that is invoking the target function;
+ * currently this will always be a FuncExpr or OpExpr.
+ *
+ * If an estimate can be made, store it into the rows field and return the
+ * address of the SupportRequestRows node.  Return NULL if no estimate can be
+ * made, in which case the planner will rely on the target function's prorows
+ * field.
+ */
+typedef struct SupportRequestRows
+{
+    NodeTag        type;
+
+    /* Input fields: */
+    struct PlannerInfo *root;    /* Planner's infrastructure (could be NULL) */
+    Oid            funcid;            /* function we are inquiring about */
+    Node       *node;            /* parse node invoking function */
+
+    /* Output fields: */
+    double        rows;            /* number of rows expected to be returned */
+} SupportRequestRows;
+
 #endif                            /* SUPPORTNODES_H */
diff --git a/src/include/optimizer/clauses.h b/src/include/optimizer/clauses.h
index 5c8580e..626fb1c 100644
--- a/src/include/optimizer/clauses.h
+++ b/src/include/optimizer/clauses.h
@@ -53,7 +53,7 @@ extern void get_agg_clause_costs(PlannerInfo *root, Node *clause,
 extern bool contain_window_function(Node *clause);
 extern WindowFuncLists *find_window_functions(Node *clause, Index maxWinRef);

-extern double expression_returns_set_rows(Node *clause);
+extern double expression_returns_set_rows(PlannerInfo *root, Node *clause);

 extern bool contain_subplans(Node *clause);

diff --git a/src/include/optimizer/plancat.h b/src/include/optimizer/plancat.h
index a1b2325..4eb2e42 100644
--- a/src/include/optimizer/plancat.h
+++ b/src/include/optimizer/plancat.h
@@ -55,6 +55,20 @@ extern Selectivity join_selectivity(PlannerInfo *root,
                  JoinType jointype,
                  SpecialJoinInfo *sjinfo);

+extern Selectivity function_selectivity(PlannerInfo *root,
+                     Oid funcid,
+                     List *args,
+                     Oid inputcollid,
+                     bool is_join,
+                     int varRelid,
+                     JoinType jointype,
+                     SpecialJoinInfo *sjinfo);
+
+extern void add_function_cost(PlannerInfo *root, Oid funcid, Node *node,
+                  QualCost *cost);
+
+extern double get_function_rows(PlannerInfo *root, Oid funcid, Node *node);
+
 extern bool has_row_triggers(PlannerInfo *root, Index rti, CmdType event);

 #endif                            /* PLANCAT_H */
diff --git a/src/include/utils/lsyscache.h b/src/include/utils/lsyscache.h
index ceec85d..16b0b1d 100644
--- a/src/include/utils/lsyscache.h
+++ b/src/include/utils/lsyscache.h
@@ -120,8 +120,7 @@ extern char func_volatile(Oid funcid);
 extern char func_parallel(Oid funcid);
 extern char get_func_prokind(Oid funcid);
 extern bool get_func_leakproof(Oid funcid);
-extern float4 get_func_cost(Oid funcid);
-extern float4 get_func_rows(Oid funcid);
+extern RegProcedure get_func_support(Oid funcid);
 extern Oid    get_relname_relid(const char *relname, Oid relnamespace);
 extern char *get_rel_name(Oid relid);
 extern Oid    get_rel_namespace(Oid relid);
diff --git a/src/test/regress/expected/misc_functions.out b/src/test/regress/expected/misc_functions.out
index 130a0e4..0879c88 100644
--- a/src/test/regress/expected/misc_functions.out
+++ b/src/test/regress/expected/misc_functions.out
@@ -133,3 +133,63 @@ ERROR:  function num_nulls() does not exist
 LINE 1: SELECT num_nulls();
                ^
 HINT:  No function matches the given name and argument types. You might need to add explicit type casts.
+--
+-- Test adding a support function to a subject function
+--
+CREATE FUNCTION my_int_eq(int, int) RETURNS bool
+  LANGUAGE internal STRICT IMMUTABLE PARALLEL SAFE
+  AS $$int4eq$$;
+-- By default, planner does not think that's selective
+EXPLAIN (COSTS OFF)
+SELECT * FROM tenk1 a JOIN tenk1 b ON a.unique1 = b.unique1
+WHERE my_int_eq(a.unique2, 42);
+                  QUERY PLAN
+----------------------------------------------
+ Hash Join
+   Hash Cond: (b.unique1 = a.unique1)
+   ->  Seq Scan on tenk1 b
+   ->  Hash
+         ->  Seq Scan on tenk1 a
+               Filter: my_int_eq(unique2, 42)
+(6 rows)
+
+-- With support function that knows it's int4eq, we get a different plan
+ALTER FUNCTION my_int_eq(int, int) SUPPORT test_support_func;
+EXPLAIN (COSTS OFF)
+SELECT * FROM tenk1 a JOIN tenk1 b ON a.unique1 = b.unique1
+WHERE my_int_eq(a.unique2, 42);
+                   QUERY PLAN
+-------------------------------------------------
+ Nested Loop
+   ->  Seq Scan on tenk1 a
+         Filter: my_int_eq(unique2, 42)
+   ->  Index Scan using tenk1_unique1 on tenk1 b
+         Index Cond: (unique1 = a.unique1)
+(5 rows)
+
+-- Also test non-default rowcount estimate
+CREATE FUNCTION my_gen_series(int, int) RETURNS SETOF integer
+  LANGUAGE internal STRICT IMMUTABLE PARALLEL SAFE
+  AS $$generate_series_int4$$
+  SUPPORT test_support_func;
+EXPLAIN (COSTS OFF)
+SELECT * FROM tenk1 a JOIN my_gen_series(1,1000) g ON a.unique1 = g;
+               QUERY PLAN
+----------------------------------------
+ Hash Join
+   Hash Cond: (g.g = a.unique1)
+   ->  Function Scan on my_gen_series g
+   ->  Hash
+         ->  Seq Scan on tenk1 a
+(5 rows)
+
+EXPLAIN (COSTS OFF)
+SELECT * FROM tenk1 a JOIN my_gen_series(1,10) g ON a.unique1 = g;
+                   QUERY PLAN
+-------------------------------------------------
+ Nested Loop
+   ->  Function Scan on my_gen_series g
+   ->  Index Scan using tenk1_unique1 on tenk1 a
+         Index Cond: (unique1 = g.g)
+(4 rows)
+
diff --git a/src/test/regress/input/create_function_1.source b/src/test/regress/input/create_function_1.source
index 26e2227..223454a 100644
--- a/src/test/regress/input/create_function_1.source
+++ b/src/test/regress/input/create_function_1.source
@@ -68,6 +68,11 @@ CREATE FUNCTION test_fdw_handler()
     AS '@libdir@/regress@DLSUFFIX@', 'test_fdw_handler'
     LANGUAGE C;

+CREATE FUNCTION test_support_func(internal)
+    RETURNS internal
+    AS '@libdir@/regress@DLSUFFIX@', 'test_support_func'
+    LANGUAGE C STRICT;
+
 -- Things that shouldn't work:

 CREATE FUNCTION test1 (int) RETURNS int LANGUAGE SQL
diff --git a/src/test/regress/output/create_function_1.source b/src/test/regress/output/create_function_1.source
index 8c50d9b..5f43e8d 100644
--- a/src/test/regress/output/create_function_1.source
+++ b/src/test/regress/output/create_function_1.source
@@ -60,6 +60,10 @@ CREATE FUNCTION test_fdw_handler()
     RETURNS fdw_handler
     AS '@libdir@/regress@DLSUFFIX@', 'test_fdw_handler'
     LANGUAGE C;
+CREATE FUNCTION test_support_func(internal)
+    RETURNS internal
+    AS '@libdir@/regress@DLSUFFIX@', 'test_support_func'
+    LANGUAGE C STRICT;
 -- Things that shouldn't work:
 CREATE FUNCTION test1 (int) RETURNS int LANGUAGE SQL
     AS 'SELECT ''not an integer'';';
diff --git a/src/test/regress/regress.c b/src/test/regress/regress.c
index 7072728..ec14f2c 100644
--- a/src/test/regress/regress.c
+++ b/src/test/regress/regress.c
@@ -23,12 +23,16 @@
 #include "access/transam.h"
 #include "access/tuptoaster.h"
 #include "access/xact.h"
+#include "catalog/pg_operator.h"
 #include "catalog/pg_type.h"
 #include "commands/sequence.h"
 #include "commands/trigger.h"
 #include "executor/executor.h"
 #include "executor/spi.h"
 #include "miscadmin.h"
+#include "nodes/supportnodes.h"
+#include "optimizer/cost.h"
+#include "optimizer/plancat.h"
 #include "port/atomics.h"
 #include "utils/builtins.h"
 #include "utils/geo_decls.h"
@@ -863,3 +867,76 @@ test_fdw_handler(PG_FUNCTION_ARGS)
     elog(ERROR, "test_fdw_handler is not implemented");
     PG_RETURN_NULL();
 }
+
+PG_FUNCTION_INFO_V1(test_support_func);
+Datum
+test_support_func(PG_FUNCTION_ARGS)
+{
+    Node       *rawreq = (Node *) PG_GETARG_POINTER(0);
+    Node       *ret = NULL;
+
+    if (IsA(rawreq, SupportRequestSelectivity))
+    {
+        /*
+         * Assume that the target is int4eq; that's safe as long as we don't
+         * attach this to any other boolean-returning function.
+         */
+        SupportRequestSelectivity *req = (SupportRequestSelectivity *) rawreq;
+        Selectivity s1;
+
+        if (req->is_join)
+            s1 = join_selectivity(req->root, Int4EqualOperator,
+                                  req->args,
+                                  req->inputcollid,
+                                  req->jointype,
+                                  req->sjinfo);
+        else
+            s1 = restriction_selectivity(req->root, Int4EqualOperator,
+                                         req->args,
+                                         req->inputcollid,
+                                         req->varRelid);
+
+        req->selectivity = s1;
+        ret = (Node *) req;
+    }
+
+    if (IsA(rawreq, SupportRequestCost))
+    {
+        /* Provide some generic estimate */
+        SupportRequestCost *req = (SupportRequestCost *) rawreq;
+
+        req->startup = 0;
+        req->per_tuple = 2 * cpu_operator_cost;
+        ret = (Node *) req;
+    }
+
+    if (IsA(rawreq, SupportRequestRows))
+    {
+        /*
+         * Assume that the target is generate_series_int4; that's safe as long
+         * as we don't attach this to any other set-returning function.
+         */
+        SupportRequestRows *req = (SupportRequestRows *) rawreq;
+
+        if (req->node && IsA(req->node, FuncExpr))    /* be paranoid */
+        {
+            List       *args = ((FuncExpr *) req->node)->args;
+            Node       *arg1 = linitial(args);
+            Node       *arg2 = lsecond(args);
+
+            if (IsA(arg1, Const) &&
+                !((Const *) arg1)->constisnull &&
+                IsA(arg2, Const) &&
+                !((Const *) arg2)->constisnull)
+            {
+                int32        val1 = DatumGetInt32(((Const *) arg1)->constvalue);
+                int32        val2 = DatumGetInt32(((Const *) arg2)->constvalue);
+
+                req->rows = val2 - val1 + 1;
+                ret = (Node *) req;
+            }
+        }
+    }
+
+    PG_RETURN_POINTER(ret);
+}
diff --git a/src/test/regress/sql/misc_functions.sql b/src/test/regress/sql/misc_functions.sql
index 1a20c1f..7a71f76 100644
--- a/src/test/regress/sql/misc_functions.sql
+++ b/src/test/regress/sql/misc_functions.sql
@@ -29,3 +29,35 @@ SELECT num_nulls(VARIADIC '{}'::int[]);
 -- should fail, one or more arguments is required
 SELECT num_nonnulls();
 SELECT num_nulls();
+
+--
+-- Test adding a support function to a subject function
+--
+
+CREATE FUNCTION my_int_eq(int, int) RETURNS bool
+  LANGUAGE internal STRICT IMMUTABLE PARALLEL SAFE
+  AS $$int4eq$$;
+
+-- By default, planner does not think that's selective
+EXPLAIN (COSTS OFF)
+SELECT * FROM tenk1 a JOIN tenk1 b ON a.unique1 = b.unique1
+WHERE my_int_eq(a.unique2, 42);
+
+-- With support function that knows it's int4eq, we get a different plan
+ALTER FUNCTION my_int_eq(int, int) SUPPORT test_support_func;
+
+EXPLAIN (COSTS OFF)
+SELECT * FROM tenk1 a JOIN tenk1 b ON a.unique1 = b.unique1
+WHERE my_int_eq(a.unique2, 42);
+
+-- Also test non-default rowcount estimate
+CREATE FUNCTION my_gen_series(int, int) RETURNS SETOF integer
+  LANGUAGE internal STRICT IMMUTABLE PARALLEL SAFE
+  AS $$generate_series_int4$$
+  SUPPORT test_support_func;
+
+EXPLAIN (COSTS OFF)
+SELECT * FROM tenk1 a JOIN my_gen_series(1,1000) g ON a.unique1 = g;
+
+EXPLAIN (COSTS OFF)
+SELECT * FROM tenk1 a JOIN my_gen_series(1,10) g ON a.unique1 = g;

pgsql-hackers by date:

Previous
From: Masahiko Sawada
Date:
Subject: Re: [HACKERS] Block level parallel vacuum
Next
From: Alvaro Herrera
Date:
Subject: Re: inherited primary key misbehavior