Deleting no-op CoerceToDomains from plan trees

classic Classic list List threaded Threaded
1 message Options
Reply | Threaded
Open this post in threaded view
|

Deleting no-op CoerceToDomains from plan trees

Tom Lane-2
Attached is a complete patch for one of the tasks discussed in [1],
namely dropping CoerceToDomain nodes from plan trees if their domains
have no constraints that need to be checked.

After I fleshed out the original patch to include sinval signaling for
changes of domain constraints, I was surprised to find that the existing
test case in domain.sql still failed.  The reason turned out to be that
the CoerceToDomain node was not being done as part of a normal plpgsql
expression, but within a cast expression cached by plpgsql's
get_cast_hashentry() function ... and that code has no provision for
cache flushes.

I concluded that the best way to fix that was to create new functionality
in plancache.c for caching expression trees with appropriate invalidation
support, so this patch includes that.  (Conceivably that should be a
separate patch, but it didn't really seem worth the trouble.)

As patched, get_cast_hashentry() is the only user of the new plancache
code.  I looked around at other callers of expression_planner() to see
if anything else was caching the result for more than one query.
I found such a caller in typcache.c's domain constraint caching, but
as discussed in [2], it's not really clear that there's any point in
changing that code; I just added a comment about the issue instead.

(I also noted some callers in relation partitioning code, which
I'm a tad suspicious of, mainly because it's not apparent to me why
that code should be executing arbitrary expressions in the first
place.  But if there's anything broken there, it seems like material
for a separate discussion.)

Some other notes for review:

* In typecmds.c, I fixed some inconsistency about whether the various
subcommands of ALTER DOMAIN released catalog locks or not.

* I used a dlist to thread CachedExpressions together in plancache.c,
and failed to resist the temptation to change CachedPlanSources to
use a dlist as well.  That eliminates a potential performance problem
in DropCachedPlan, though I'm not sure how important that is.

                        regards, tom lane

[1] https://www.postgresql.org/message-id/5978.1544030694@...
[2] https://www.postgresql.org/message-id/12539.1544107316@...


diff --git a/src/backend/commands/typecmds.c b/src/backend/commands/typecmds.c
index 1ffc823..544f423 100644
*** a/src/backend/commands/typecmds.c
--- b/src/backend/commands/typecmds.c
***************
*** 62,67 ****
--- 62,68 ----
  #include "parser/parse_type.h"
  #include "utils/builtins.h"
  #include "utils/fmgroids.h"
+ #include "utils/inval.h"
  #include "utils/lsyscache.h"
  #include "utils/memutils.h"
  #include "utils/rel.h"
*************** AlterDomainDefault(List *names, Node *de
*** 2297,2303 ****
  ObjectAddressSet(address, TypeRelationId, domainoid);
 
  /* Clean up */
! heap_close(rel, NoLock);
  heap_freetuple(newtuple);
 
  return address;
--- 2298,2304 ----
  ObjectAddressSet(address, TypeRelationId, domainoid);
 
  /* Clean up */
! heap_close(rel, RowExclusiveLock);
  heap_freetuple(newtuple);
 
  return address;
*************** AlterDomainDropConstraint(List *names, c
*** 2494,2501 ****
  systable_endscan(conscan);
  heap_close(conrel, RowExclusiveLock);
 
- heap_close(rel, NoLock);
-
  if (!found)
  {
  if (!missing_ok)
--- 2495,2500 ----
*************** AlterDomainDropConstraint(List *names, c
*** 2509,2516 ****
--- 2508,2525 ----
  constrName, TypeNameToString(typename))));
  }
 
+ /*
+ * We must send out an sinval message for the domain, to ensure that any
+ * dependent plans get rebuilt.  Since this command doesn't change the
+ * domain's pg_type row, that won't happen automatically; do it manually.
+ */
+ CacheInvalidateHeapTuple(rel, tup, NULL);
+
  ObjectAddressSet(address, TypeRelationId, domainoid);
 
+ /* Clean up */
+ heap_close(rel, RowExclusiveLock);
+
  return address;
  }
 
*************** AlterDomainAddConstraint(List *names, No
*** 2615,2620 ****
--- 2624,2636 ----
  if (!constr->skip_validation)
  validateDomainConstraint(domainoid, ccbin);
 
+ /*
+ * We must send out an sinval message for the domain, to ensure that any
+ * dependent plans get rebuilt.  Since this command doesn't change the
+ * domain's pg_type row, that won't happen automatically; do it manually.
+ */
+ CacheInvalidateHeapTuple(typrel, tup, NULL);
+
  ObjectAddressSet(address, TypeRelationId, domainoid);
 
  /* Clean up */
diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c
index c729a99..b645648 100644
*** a/src/backend/optimizer/plan/planner.c
--- b/src/backend/optimizer/plan/planner.c
*************** adjust_paths_for_srfs(PlannerInfo *root,
*** 5923,5932 ****
   * side-effect that is useful when the expression will get evaluated more than
   * once.  Also, we must fix operator function IDs.
   *
   * Note: this must not make any damaging changes to the passed-in expression
   * tree.  (It would actually be okay to apply fix_opfuncids to it, but since
   * we first do an expression_tree_mutator-based walk, what is returned will
!  * be a new node tree.)
   */
  Expr *
  expression_planner(Expr *expr)
--- 5923,5938 ----
   * side-effect that is useful when the expression will get evaluated more than
   * once.  Also, we must fix operator function IDs.
   *
+  * This does not return any information about dependencies of the expression.
+  * Hence callers should use the results only for the duration of the current
+  * query.  Callers that would like to cache the results for longer should use
+  * expression_planner_with_deps, probably via the plancache.
+  *
   * Note: this must not make any damaging changes to the passed-in expression
   * tree.  (It would actually be okay to apply fix_opfuncids to it, but since
   * we first do an expression_tree_mutator-based walk, what is returned will
!  * be a new node tree.)  The result is constructed in the current memory
!  * context; beware that this can leak a lot of additional stuff there, too.
   */
  Expr *
  expression_planner(Expr *expr)
*************** expression_planner(Expr *expr)
*** 5945,5950 ****
--- 5951,6007 ----
  return (Expr *) result;
  }
 
+ /*
+  * expression_planner_with_deps
+  * Perform planner's transformations on a standalone expression,
+  * returning expression dependency information along with the result.
+  *
+  * This is identical to expression_planner() except that it also returns
+  * information about possible dependencies of the expression, ie identities of
+  * objects whose definitions affect the result.  As in a PlannedStmt, these
+  * are expressed as a list of relation Oids and a list of PlanInvalItems.
+  */
+ Expr *
+ expression_planner_with_deps(Expr *expr,
+ List **relationOids,
+ List **invalItems)
+ {
+ Node   *result;
+ PlannerGlobal glob;
+ PlannerInfo root;
+
+ /* Make up dummy planner state so we can use setrefs machinery */
+ MemSet(&glob, 0, sizeof(glob));
+ glob.type = T_PlannerGlobal;
+ glob.relationOids = NIL;
+ glob.invalItems = NIL;
+
+ MemSet(&root, 0, sizeof(root));
+ root.type = T_PlannerInfo;
+ root.glob = &glob;
+
+ /*
+ * Convert named-argument function calls, insert default arguments and
+ * simplify constant subexprs.  Collect identities of inlined functions
+ * and elided domains, too.
+ */
+ result = eval_const_expressions(&root, (Node *) expr);
+
+ /* Fill in opfuncid values if missing */
+ fix_opfuncids(result);
+
+ /*
+ * Now walk the finished expression to find anything else we ought to
+ * record as an expression dependency.
+ */
+ (void) extract_query_dependencies_walker(result, &root);
+
+ *relationOids = glob.relationOids;
+ *invalItems = glob.invalItems;
+
+ return (Expr *) result;
+ }
+
 
  /*
   * plan_cluster_use_sort
diff --git a/src/backend/optimizer/plan/setrefs.c b/src/backend/optimizer/plan/setrefs.c
index 6d6ef1c..a718e15 100644
*** a/src/backend/optimizer/plan/setrefs.c
--- b/src/backend/optimizer/plan/setrefs.c
*************** static List *set_returning_clause_refere
*** 138,145 ****
  Plan *topplan,
  Index resultRelation,
  int rtoffset);
! static bool extract_query_dependencies_walker(Node *node,
!  PlannerInfo *context);
 
  /*****************************************************************************
   *
--- 138,144 ----
  Plan *topplan,
  Index resultRelation,
  int rtoffset);
!
 
  /*****************************************************************************
   *
*************** static bool extract_query_dependencies_w
*** 175,182 ****
   * This will be used by plancache.c to drive invalidation of cached plans.
   * Relation dependencies are represented by OIDs, and everything else by
   * PlanInvalItems (this distinction is motivated by the shared-inval APIs).
!  * Currently, relations and user-defined functions are the only types of
!  * objects that are explicitly tracked this way.
   *
   * 8. We assign every plan node in the tree a unique ID.
   *
--- 174,181 ----
   * This will be used by plancache.c to drive invalidation of cached plans.
   * Relation dependencies are represented by OIDs, and everything else by
   * PlanInvalItems (this distinction is motivated by the shared-inval APIs).
!  * Currently, relations, user-defined functions, and domains are the only
!  * types of objects that are explicitly tracked this way.
   *
   * 8. We assign every plan node in the tree a unique ID.
   *
*************** record_plan_function_dependency(PlannerI
*** 2576,2581 ****
--- 2575,2616 ----
  }
 
  /*
+  * record_plan_type_dependency
+  * Mark the current plan as depending on a particular type.
+  *
+  * This is exported so that eval_const_expressions can record a
+  * dependency on a domain that it's removed a CoerceToDomain node for.
+  *
+  * We don't currently need to record dependencies on domains that the
+  * plan contains CoerceToDomain nodes for, though that might change in
+  * future.  Hence, this isn't actually called in this module, though
+  * someday fix_expr_common might call it.
+  */
+ void
+ record_plan_type_dependency(PlannerInfo *root, Oid typeid)
+ {
+ /*
+ * As in record_plan_function_dependency, ignore the possibility that
+ * someone would change a built-in domain.
+ */
+ if (typeid >= (Oid) FirstBootstrapObjectId)
+ {
+ PlanInvalItem *inval_item = makeNode(PlanInvalItem);
+
+ /*
+ * It would work to use any syscache on pg_type, but the easiest is
+ * TYPEOID since we already have the type's OID at hand.  Note that
+ * plancache.c knows we use TYPEOID.
+ */
+ inval_item->cacheId = TYPEOID;
+ inval_item->hashValue = GetSysCacheHashValue1(TYPEOID,
+  ObjectIdGetDatum(typeid));
+
+ root->glob->invalItems = lappend(root->glob->invalItems, inval_item);
+ }
+ }
+
+ /*
   * extract_query_dependencies
   * Given a rewritten, but not yet planned, query or queries
   * (i.e. a Query node or list of Query nodes), extract dependencies
*************** record_plan_function_dependency(PlannerI
*** 2584,2589 ****
--- 2619,2631 ----
   *
   * This is needed by plancache.c to handle invalidation of cached unplanned
   * queries.
+  *
+  * Note: this does not go through eval_const_expressions, and hence doesn't
+  * reflect its additions of inlined functions and elided CoerceToDomain nodes
+  * to the invalItems list.  This is obviously OK for functions, since we'll
+  * see them in the original query tree anyway.  For domains, it's OK because
+  * we don't care about domains unless they get elided.  That is, a plan might
+  * have domain dependencies that the query tree doesn't.
   */
  void
  extract_query_dependencies(Node *query,
*************** extract_query_dependencies(Node *query,
*** 2613,2626 ****
  *hasRowSecurity = glob.dependsOnRole;
  }
 
! static bool
  extract_query_dependencies_walker(Node *node, PlannerInfo *context)
  {
  if (node == NULL)
  return false;
  Assert(!IsA(node, PlaceHolderVar));
- /* Extract function dependencies and check for regclass Consts */
- fix_expr_common(context, node);
  if (IsA(node, Query))
  {
  Query   *query = (Query *) node;
--- 2655,2674 ----
  *hasRowSecurity = glob.dependsOnRole;
  }
 
! /*
!  * Tree walker for extract_query_dependencies.
!  *
!  * This is exported so that expression_planner_with_deps can call it on
!  * simple expressions (post-planning, not before planning, in that case).
!  * In that usage, glob.dependsOnRole isn't meaningful, but the relationOids
!  * and invalItems lists are added to as needed.
!  */
! bool
  extract_query_dependencies_walker(Node *node, PlannerInfo *context)
  {
  if (node == NULL)
  return false;
  Assert(!IsA(node, PlaceHolderVar));
  if (IsA(node, Query))
  {
  Query   *query = (Query *) node;
*************** extract_query_dependencies_walker(Node *
*** 2660,2665 ****
--- 2708,2715 ----
  return query_tree_walker(query, extract_query_dependencies_walker,
  (void *) context, 0);
  }
+ /* Extract function dependencies and check for regclass Consts */
+ fix_expr_common(context, node);
  return expression_tree_walker(node, extract_query_dependencies_walker,
   (void *) context);
  }
diff --git a/src/backend/optimizer/util/clauses.c b/src/backend/optimizer/util/clauses.c
index 8df3693..f444616 100644
*** a/src/backend/optimizer/util/clauses.c
--- b/src/backend/optimizer/util/clauses.c
*************** eval_const_expressions_mutator(Node *nod
*** 3699,3704 ****
--- 3699,3768 ----
  newbtest->location = btest->location;
  return (Node *) newbtest;
  }
+ case T_CoerceToDomain:
+ {
+ /*
+ * If the domain currently has no constraints, we replace the
+ * CoerceToDomain node with a simple RelabelType, which is
+ * both far faster to execute and more amenable to later
+ * optimization.  We must then mark the plan as needing to be
+ * rebuilt if the domain's constraints change.
+ *
+ * Also, in estimation mode, always replace CoerceToDomain
+ * nodes, effectively assuming that the coercion will succeed.
+ */
+ CoerceToDomain *cdomain = (CoerceToDomain *) node;
+ CoerceToDomain *newcdomain;
+ Node   *arg;
+
+ arg = eval_const_expressions_mutator((Node *) cdomain->arg,
+ context);
+ if (context->estimate ||
+ !DomainHasConstraints(cdomain->resulttype))
+ {
+ /* Record dependency, if this isn't estimation mode */
+ if (context->root && !context->estimate)
+ record_plan_type_dependency(context->root,
+ cdomain->resulttype);
+
+ /* Generate RelabelType to substitute for CoerceToDomain */
+ /* This should match the RelabelType logic above */
+
+ while (arg && IsA(arg, RelabelType))
+ arg = (Node *) ((RelabelType *) arg)->arg;
+
+ if (arg && IsA(arg, Const))
+ {
+ Const   *con = (Const *) arg;
+
+ con->consttype = cdomain->resulttype;
+ con->consttypmod = cdomain->resulttypmod;
+ con->constcollid = cdomain->resultcollid;
+ return (Node *) con;
+ }
+ else
+ {
+ RelabelType *newrelabel = makeNode(RelabelType);
+
+ newrelabel->arg = (Expr *) arg;
+ newrelabel->resulttype = cdomain->resulttype;
+ newrelabel->resulttypmod = cdomain->resulttypmod;
+ newrelabel->resultcollid = cdomain->resultcollid;
+ newrelabel->relabelformat = cdomain->coercionformat;
+ newrelabel->location = cdomain->location;
+ return (Node *) newrelabel;
+ }
+ }
+
+ newcdomain = makeNode(CoerceToDomain);
+ newcdomain->arg = (Expr *) arg;
+ newcdomain->resulttype = cdomain->resulttype;
+ newcdomain->resulttypmod = cdomain->resulttypmod;
+ newcdomain->resultcollid = cdomain->resultcollid;
+ newcdomain->coercionformat = cdomain->coercionformat;
+ newcdomain->location = cdomain->location;
+ return (Node *) newcdomain;
+ }
  case T_PlaceHolderVar:
 
  /*
*************** eval_const_expressions_mutator(Node *nod
*** 3770,3776 ****
  * For any node type not handled above, copy the node unchanged but
  * const-simplify its subexpressions.  This is the correct thing for node
  * types whose behavior might change between planning and execution, such
! * as CoerceToDomain.  It's also a safe default for new node types not
  * known to this routine.
  */
  return ece_generic_processing(node);
--- 3834,3840 ----
  * For any node type not handled above, copy the node unchanged but
  * const-simplify its subexpressions.  This is the correct thing for node
  * types whose behavior might change between planning and execution, such
! * as CurrentOfExpr.  It's also a safe default for new node types not
  * known to this routine.
  */
  return ece_generic_processing(node);
diff --git a/src/backend/utils/cache/plancache.c b/src/backend/utils/cache/plancache.c
index 9ec81c5..a3898ec 100644
*** a/src/backend/utils/cache/plancache.c
--- b/src/backend/utils/cache/plancache.c
***************
*** 27,41 ****
   * query to change output tupdesc on replan --- if so, it's up to the
   * caller to notice changes and cope with them.
   *
!  * Currently, we track exactly the dependencies of plans on relations and
!  * user-defined functions.  On relcache invalidation events or pg_proc
!  * syscache invalidation events, we invalidate just those plans that depend
!  * on the particular object being modified.  (Note: this scheme assumes
!  * that any table modification that requires replanning will generate a
!  * relcache inval event.)  We also watch for inval events on certain other
!  * system catalogs, such as pg_namespace; but for them, our response is
!  * just to invalidate all plans.  We expect updates on those catalogs to
!  * be infrequent enough that more-detailed tracking is not worth the effort.
   *
   *
   * Portions Copyright (c) 1996-2018, PostgreSQL Global Development Group
--- 27,47 ----
   * query to change output tupdesc on replan --- if so, it's up to the
   * caller to notice changes and cope with them.
   *
!  * Currently, we track exactly the dependencies of plans on relations,
!  * user-defined functions, and domains.  On relcache invalidation events or
!  * pg_proc or pg_type syscache invalidation events, we invalidate just those
!  * plans that depend on the particular object being modified.  (Note: this
!  * scheme assumes that any table modification that requires replanning will
!  * generate a relcache inval event.)  We also watch for inval events on
!  * certain other system catalogs, such as pg_namespace; but for them, our
!  * response is just to invalidate all plans.  We expect updates on those
!  * catalogs to be infrequent enough that more-detailed tracking is not worth
!  * the effort.
!  *
!  * In addition to full-fledged query plans, we provide a facility for
!  * detecting invalidations of simple scalar expressions.  This is fairly
!  * bare-bones; it's the caller's responsibility to build a new expression
!  * if the old one gets invalidated.
   *
   *
   * Portions Copyright (c) 1996-2018, PostgreSQL Global Development Group
***************
*** 57,62 ****
--- 63,69 ----
  #include "nodes/nodeFuncs.h"
  #include "optimizer/cost.h"
  #include "optimizer/planmain.h"
+ #include "optimizer/planner.h"
  #include "optimizer/prep.h"
  #include "parser/analyze.h"
  #include "parser/parsetree.h"
***************
*** 82,91 ****
  /*
   * This is the head of the backend's list of "saved" CachedPlanSources (i.e.,
   * those that are in long-lived storage and are examined for sinval events).
!  * We thread the structs manually instead of using List cells so that we can
!  * guarantee to save a CachedPlanSource without error.
   */
! static CachedPlanSource *first_saved_plan = NULL;
 
  static void ReleaseGenericPlan(CachedPlanSource *plansource);
  static List *RevalidateCachedQuery(CachedPlanSource *plansource,
--- 89,103 ----
  /*
   * This is the head of the backend's list of "saved" CachedPlanSources (i.e.,
   * those that are in long-lived storage and are examined for sinval events).
!  * We use a dlist instead of separate List cells so that we can guarantee
!  * to save a CachedPlanSource without error.
   */
! static dlist_head saved_plan_list;
!
! /*
!  * This is the head of the backend's list of CachedExpressions.
!  */
! static dlist_head cached_expression_list;
 
  static void ReleaseGenericPlan(CachedPlanSource *plansource);
  static List *RevalidateCachedQuery(CachedPlanSource *plansource,
*************** static void ScanQueryForLocks(Query *par
*** 103,109 ****
  static bool ScanQueryWalker(Node *node, bool *acquire);
  static TupleDesc PlanCacheComputeResultDesc(List *stmt_list);
  static void PlanCacheRelCallback(Datum arg, Oid relid);
! static void PlanCacheFuncCallback(Datum arg, int cacheid, uint32 hashvalue);
  static void PlanCacheSysCallback(Datum arg, int cacheid, uint32 hashvalue);
 
  /* GUC parameter */
--- 115,121 ----
  static bool ScanQueryWalker(Node *node, bool *acquire);
  static TupleDesc PlanCacheComputeResultDesc(List *stmt_list);
  static void PlanCacheRelCallback(Datum arg, Oid relid);
! static void PlanCacheObjectCallback(Datum arg, int cacheid, uint32 hashvalue);
  static void PlanCacheSysCallback(Datum arg, int cacheid, uint32 hashvalue);
 
  /* GUC parameter */
*************** void
*** 118,124 ****
  InitPlanCache(void)
  {
  CacheRegisterRelcacheCallback(PlanCacheRelCallback, (Datum) 0);
! CacheRegisterSyscacheCallback(PROCOID, PlanCacheFuncCallback, (Datum) 0);
  CacheRegisterSyscacheCallback(NAMESPACEOID, PlanCacheSysCallback, (Datum) 0);
  CacheRegisterSyscacheCallback(OPEROID, PlanCacheSysCallback, (Datum) 0);
  CacheRegisterSyscacheCallback(AMOPOPID, PlanCacheSysCallback, (Datum) 0);
--- 130,137 ----
  InitPlanCache(void)
  {
  CacheRegisterRelcacheCallback(PlanCacheRelCallback, (Datum) 0);
! CacheRegisterSyscacheCallback(PROCOID, PlanCacheObjectCallback, (Datum) 0);
! CacheRegisterSyscacheCallback(TYPEOID, PlanCacheObjectCallback, (Datum) 0);
  CacheRegisterSyscacheCallback(NAMESPACEOID, PlanCacheSysCallback, (Datum) 0);
  CacheRegisterSyscacheCallback(OPEROID, PlanCacheSysCallback, (Datum) 0);
  CacheRegisterSyscacheCallback(AMOPOPID, PlanCacheSysCallback, (Datum) 0);
*************** CreateCachedPlan(RawStmt *raw_parse_tree
*** 206,212 ****
  plansource->is_saved = false;
  plansource->is_valid = false;
  plansource->generation = 0;
- plansource->next_saved = NULL;
  plansource->generic_cost = -1;
  plansource->total_custom_cost = 0;
  plansource->num_custom_plans = 0;
--- 219,224 ----
*************** CreateOneShotCachedPlan(RawStmt *raw_par
*** 274,280 ****
  plansource->is_saved = false;
  plansource->is_valid = false;
  plansource->generation = 0;
- plansource->next_saved = NULL;
  plansource->generic_cost = -1;
  plansource->total_custom_cost = 0;
  plansource->num_custom_plans = 0;
--- 286,291 ----
*************** SaveCachedPlan(CachedPlanSource *plansou
*** 471,478 ****
  /*
  * Add the entry to the global list of cached plans.
  */
! plansource->next_saved = first_saved_plan;
! first_saved_plan = plansource;
 
  plansource->is_saved = true;
  }
--- 482,488 ----
  /*
  * Add the entry to the global list of cached plans.
  */
! dlist_push_tail(&saved_plan_list, &plansource->node);
 
  plansource->is_saved = true;
  }
*************** DropCachedPlan(CachedPlanSource *plansou
*** 493,513 ****
  /* If it's been saved, remove it from the list */
  if (plansource->is_saved)
  {
! if (first_saved_plan == plansource)
! first_saved_plan = plansource->next_saved;
! else
! {
! CachedPlanSource *psrc;
!
! for (psrc = first_saved_plan; psrc; psrc = psrc->next_saved)
! {
! if (psrc->next_saved == plansource)
! {
! psrc->next_saved = plansource->next_saved;
! break;
! }
! }
! }
  plansource->is_saved = false;
  }
 
--- 503,509 ----
  /* If it's been saved, remove it from the list */
  if (plansource->is_saved)
  {
! dlist_delete(&plansource->node);
  plansource->is_saved = false;
  }
 
*************** CopyCachedPlan(CachedPlanSource *plansou
*** 1399,1405 ****
  newsource->is_saved = false;
  newsource->is_valid = plansource->is_valid;
  newsource->generation = plansource->generation;
- newsource->next_saved = NULL;
 
  /* We may as well copy any acquired cost knowledge */
  newsource->generic_cost = plansource->generic_cost;
--- 1395,1400 ----
*************** CachedPlanGetTargetList(CachedPlanSource
*** 1459,1464 ****
--- 1454,1538 ----
  }
 
  /*
+  * GetCachedExpression: construct a CachedExpression for an expression.
+  *
+  * This performs the same transformations on the expression as
+  * expression_planner(), ie, convert an expression as emitted by parse
+  * analysis to be ready to pass to the executor.
+  *
+  * The result is stashed in a private, long-lived memory context.
+  * (Note that this might leak a good deal of memory in the caller's
+  * context before that.)  The passed-in expr tree is not modified.
+  */
+ CachedExpression *
+ GetCachedExpression(Node *expr)
+ {
+ CachedExpression *cexpr;
+ List   *relationOids;
+ List   *invalItems;
+ MemoryContext cexpr_context;
+ MemoryContext oldcxt;
+
+ /*
+ * Pass the expression through the planner, and collect dependencies.
+ * Everything built here is leaked in the caller's context; that's
+ * intentional to minimize the size of the permanent data structure.
+ */
+ expr = (Node *) expression_planner_with_deps((Expr *) expr,
+ &relationOids,
+ &invalItems);
+
+ /*
+ * Make a private memory context, and copy what we need into that.  To
+ * avoid leaking a long-lived context if we fail while copying data, we
+ * initially make the context under the caller's context.
+ */
+ cexpr_context = AllocSetContextCreate(CurrentMemoryContext,
+  "CachedExpression",
+  ALLOCSET_SMALL_SIZES);
+
+ oldcxt = MemoryContextSwitchTo(cexpr_context);
+
+ cexpr = (CachedExpression *) palloc(sizeof(CachedExpression));
+ cexpr->magic = CACHEDEXPR_MAGIC;
+ cexpr->expr = copyObject(expr);
+ cexpr->is_valid = true;
+ cexpr->relationOids = copyObject(relationOids);
+ cexpr->invalItems = copyObject(invalItems);
+ cexpr->context = cexpr_context;
+
+ MemoryContextSwitchTo(oldcxt);
+
+ /*
+ * Reparent the expr's memory context under CacheMemoryContext so that it
+ * will live indefinitely.
+ */
+ MemoryContextSetParent(cexpr->context, CacheMemoryContext);
+
+ /*
+ * Add the entry to the global list of cached expressions.
+ */
+ dlist_push_tail(&cached_expression_list, &cexpr->node);
+
+ return cexpr;
+ }
+
+ /*
+  * FreeCachedExpression
+  * Delete a CachedExpression.
+  */
+ void
+ FreeCachedExpression(CachedExpression *cexpr)
+ {
+ /* Sanity check */
+ Assert(cexpr->magic == CACHEDEXPR_MAGIC);
+ /* Unlink from global list */
+ dlist_delete(&cexpr->node);
+ /* Free all storage associated with CachedExpression */
+ MemoryContextDelete(cexpr->context);
+ }
+
+ /*
   * QueryListGetPrimaryStmt
   * Get the "primary" stmt within a list, ie, the one marked canSetTag.
   *
*************** PlanCacheComputeResultDesc(List *stmt_li
*** 1692,1701 ****
  static void
  PlanCacheRelCallback(Datum arg, Oid relid)
  {
! CachedPlanSource *plansource;
 
! for (plansource = first_saved_plan; plansource; plansource = plansource->next_saved)
  {
  Assert(plansource->magic == CACHEDPLANSOURCE_MAGIC);
 
  /* No work if it's already invalidated */
--- 1766,1778 ----
  static void
  PlanCacheRelCallback(Datum arg, Oid relid)
  {
! dlist_iter iter;
 
! dlist_foreach(iter, &saved_plan_list)
  {
+ CachedPlanSource *plansource = dlist_container(CachedPlanSource,
+   node, iter.cur);
+
  Assert(plansource->magic == CACHEDPLANSOURCE_MAGIC);
 
  /* No work if it's already invalidated */
*************** PlanCacheRelCallback(Datum arg, Oid reli
*** 1742,1766 ****
  }
  }
  }
  }
 
  /*
!  * PlanCacheFuncCallback
!  * Syscache inval callback function for PROCOID cache
   *
   * Invalidate all plans mentioning the object with the specified hash value,
   * or all plans mentioning any member of this cache if hashvalue == 0.
-  *
-  * Note that the coding would support use for multiple caches, but right
-  * now only user-defined functions are tracked this way.
   */
  static void
! PlanCacheFuncCallback(Datum arg, int cacheid, uint32 hashvalue)
  {
! CachedPlanSource *plansource;
 
! for (plansource = first_saved_plan; plansource; plansource = plansource->next_saved)
  {
  ListCell   *lc;
 
  Assert(plansource->magic == CACHEDPLANSOURCE_MAGIC);
--- 1819,1861 ----
  }
  }
  }
+
+ /* Likewise check cached expressions */
+ dlist_foreach(iter, &cached_expression_list)
+ {
+ CachedExpression *cexpr = dlist_container(CachedExpression,
+  node, iter.cur);
+
+ Assert(cexpr->magic == CACHEDEXPR_MAGIC);
+
+ /* No work if it's already invalidated */
+ if (!cexpr->is_valid)
+ continue;
+
+ if ((relid == InvalidOid) ? cexpr->relationOids != NIL :
+ list_member_oid(cexpr->relationOids, relid))
+ {
+ cexpr->is_valid = false;
+ }
+ }
  }
 
  /*
!  * PlanCacheObjectCallback
!  * Syscache inval callback function for PROCOID and TYPEOID caches
   *
   * Invalidate all plans mentioning the object with the specified hash value,
   * or all plans mentioning any member of this cache if hashvalue == 0.
   */
  static void
! PlanCacheObjectCallback(Datum arg, int cacheid, uint32 hashvalue)
  {
! dlist_iter iter;
 
! dlist_foreach(iter, &saved_plan_list)
  {
+ CachedPlanSource *plansource = dlist_container(CachedPlanSource,
+   node, iter.cur);
  ListCell   *lc;
 
  Assert(plansource->magic == CACHEDPLANSOURCE_MAGIC);
*************** PlanCacheFuncCallback(Datum arg, int cac
*** 1825,1830 ****
--- 1920,1953 ----
  }
  }
  }
+
+ /* Likewise check cached expressions */
+ dlist_foreach(iter, &cached_expression_list)
+ {
+ CachedExpression *cexpr = dlist_container(CachedExpression,
+  node, iter.cur);
+ ListCell   *lc;
+
+ Assert(cexpr->magic == CACHEDEXPR_MAGIC);
+
+ /* No work if it's already invalidated */
+ if (!cexpr->is_valid)
+ continue;
+
+ foreach(lc, cexpr->invalItems)
+ {
+ PlanInvalItem *item = (PlanInvalItem *) lfirst(lc);
+
+ if (item->cacheId != cacheid)
+ continue;
+ if (hashvalue == 0 ||
+ item->hashValue == hashvalue)
+ {
+ cexpr->is_valid = false;
+ break;
+ }
+ }
+ }
  }
 
  /*
*************** PlanCacheSysCallback(Datum arg, int cach
*** 1845,1854 ****
  void
  ResetPlanCache(void)
  {
! CachedPlanSource *plansource;
 
! for (plansource = first_saved_plan; plansource; plansource = plansource->next_saved)
  {
  ListCell   *lc;
 
  Assert(plansource->magic == CACHEDPLANSOURCE_MAGIC);
--- 1968,1979 ----
  void
  ResetPlanCache(void)
  {
! dlist_iter iter;
 
! dlist_foreach(iter, &saved_plan_list)
  {
+ CachedPlanSource *plansource = dlist_container(CachedPlanSource,
+   node, iter.cur);
  ListCell   *lc;
 
  Assert(plansource->magic == CACHEDPLANSOURCE_MAGIC);
*************** ResetPlanCache(void)
*** 1888,1891 ****
--- 2013,2027 ----
  }
  }
  }
+
+ /* Likewise invalidate cached expressions */
+ dlist_foreach(iter, &cached_expression_list)
+ {
+ CachedExpression *cexpr = dlist_container(CachedExpression,
+  node, iter.cur);
+
+ Assert(cexpr->magic == CACHEDEXPR_MAGIC);
+
+ cexpr->is_valid = false;
+ }
  }
diff --git a/src/backend/utils/cache/typcache.c b/src/backend/utils/cache/typcache.c
index 09f9d5f..1a96cc9 100644
*** a/src/backend/utils/cache/typcache.c
--- b/src/backend/utils/cache/typcache.c
*************** load_domaintype_info(TypeCacheEntry *typ
*** 992,998 ****
 
  check_expr = (Expr *) stringToNode(constring);
 
! /* ExecInitExpr will assume we've planned the expression */
  check_expr = expression_planner(check_expr);
 
  r = makeNode(DomainConstraintState);
--- 992,1007 ----
 
  check_expr = (Expr *) stringToNode(constring);
 
! /*
! * Plan the expression, since ExecInitExpr will expect that.
! *
! * Note: caching the result of expression_planner() is not very
! * good practice.  Ideally we'd use a CachedExpression here so
! * that we would react promptly to, eg, changes in inlined
! * functions.  However, because we don't support mutable domain
! * CHECK constraints, it's not really clear that it's worth the
! * extra overhead to do that.
! */
  check_expr = expression_planner(check_expr);
 
  r = makeNode(DomainConstraintState);
diff --git a/src/include/optimizer/planmain.h b/src/include/optimizer/planmain.h
index c8ab028..2b1932d 100644
*** a/src/include/optimizer/planmain.h
--- b/src/include/optimizer/planmain.h
*************** extern bool innerrel_is_unique(PlannerIn
*** 116,124 ****
--- 116,126 ----
   */
  extern Plan *set_plan_references(PlannerInfo *root, Plan *plan);
  extern void record_plan_function_dependency(PlannerInfo *root, Oid funcid);
+ extern void record_plan_type_dependency(PlannerInfo *root, Oid typeid);
  extern void extract_query_dependencies(Node *query,
    List **relationOids,
    List **invalItems,
    bool *hasRowSecurity);
+ extern bool extract_query_dependencies_walker(Node *node, PlannerInfo *root);
 
  #endif /* PLANMAIN_H */
diff --git a/src/include/optimizer/planner.h b/src/include/optimizer/planner.h
index 3e733b3..902ab40 100644
*** a/src/include/optimizer/planner.h
--- b/src/include/optimizer/planner.h
*************** extern Path *get_cheapest_fractional_pat
*** 55,60 ****
--- 55,63 ----
  double tuple_fraction);
 
  extern Expr *expression_planner(Expr *expr);
+ extern Expr *expression_planner_with_deps(Expr *expr,
+ List **relationOids,
+ List **invalItems);
 
  extern Expr *preprocess_phv_expression(PlannerInfo *root, Expr *expr);
 
diff --git a/src/include/utils/plancache.h b/src/include/utils/plancache.h
index 5fc7903..7fd7ea6 100644
*** a/src/include/utils/plancache.h
--- b/src/include/utils/plancache.h
***************
*** 16,29 ****
--- 16,42 ----
  #define PLANCACHE_H
 
  #include "access/tupdesc.h"
+ #include "lib/ilist.h"
  #include "nodes/params.h"
  #include "utils/queryenvironment.h"
 
  /* Forward declaration, to avoid including parsenodes.h here */
  struct RawStmt;
 
+ /* possible values for plan_cache_mode */
+ typedef enum
+ {
+ PLAN_CACHE_MODE_AUTO,
+ PLAN_CACHE_MODE_FORCE_GENERIC_PLAN,
+ PLAN_CACHE_MODE_FORCE_CUSTOM_PLAN
+ } PlanCacheMode;
+
+ /* GUC parameter */
+ extern int plan_cache_mode;
+
  #define CACHEDPLANSOURCE_MAGIC 195726186
  #define CACHEDPLAN_MAGIC 953717834
+ #define CACHEDEXPR_MAGIC 838275847
 
  /*
   * CachedPlanSource (which might better have been called CachedQuery)
*************** typedef struct CachedPlanSource
*** 110,116 ****
  bool is_valid; /* is the query_list currently valid? */
  int generation; /* increments each time we create a plan */
  /* If CachedPlanSource has been saved, it is a member of a global list */
! struct CachedPlanSource *next_saved; /* list link, if so */
  /* State kept to help decide whether to use custom or generic plans: */
  double generic_cost; /* cost of generic plan, or -1 if not known */
  double total_custom_cost; /* total cost of custom plans so far */
--- 123,129 ----
  bool is_valid; /* is the query_list currently valid? */
  int generation; /* increments each time we create a plan */
  /* If CachedPlanSource has been saved, it is a member of a global list */
! dlist_node node; /* list link, if is_saved */
  /* State kept to help decide whether to use custom or generic plans: */
  double generic_cost; /* cost of generic plan, or -1 if not known */
  double total_custom_cost; /* total cost of custom plans so far */
*************** typedef struct CachedPlan
*** 143,148 ****
--- 156,185 ----
  MemoryContext context; /* context containing this CachedPlan */
  } CachedPlan;
 
+ /*
+  * CachedExpression is a low-overhead mechanism for caching the planned form
+  * of standalone scalar expressions.  While such expressions are not usually
+  * subject to cache invalidation events, that can happen, for example because
+  * of replacement of a SQL function that was inlined into the expression.
+  * The plancache takes care of storing the expression tree and marking it
+  * invalid if a cache invalidation occurs, but the caller must notice the
+  * !is_valid status and discard the obsolete expression without reusing it.
+  * We do not store the original parse tree, only the planned expression;
+  * this is an optimization based on the assumption that we usually will not
+  * need to replan for the life of the session.
+  */
+ typedef struct CachedExpression
+ {
+ int magic; /* should equal CACHEDEXPR_MAGIC */
+ Node   *expr; /* planned form of expression */
+ bool is_valid; /* is the expression still valid? */
+ /* remaining fields should be treated as private to plancache.c: */
+ List   *relationOids; /* OIDs of relations the expr depends on */
+ List   *invalItems; /* other dependencies, as PlanInvalItems */
+ MemoryContext context; /* context containing this CachedExpression */
+ dlist_node node; /* link in global list of CachedExpressions */
+ } CachedExpression;
+
 
  extern void InitPlanCache(void);
  extern void ResetPlanCache(void);
*************** extern CachedPlan *GetCachedPlan(CachedP
*** 182,196 ****
   QueryEnvironment *queryEnv);
  extern void ReleaseCachedPlan(CachedPlan *plan, bool useResOwner);
 
! /* possible values for plan_cache_mode */
! typedef enum
! {
! PLAN_CACHE_MODE_AUTO,
! PLAN_CACHE_MODE_FORCE_GENERIC_PLAN,
! PLAN_CACHE_MODE_FORCE_CUSTOM_PLAN
! } PlanCacheMode;
!
! /* GUC parameter */
! extern int plan_cache_mode;
 
  #endif /* PLANCACHE_H */
--- 219,225 ----
   QueryEnvironment *queryEnv);
  extern void ReleaseCachedPlan(CachedPlan *plan, bool useResOwner);
 
! extern CachedExpression *GetCachedExpression(Node *expr);
! extern void FreeCachedExpression(CachedExpression *cexpr);
 
  #endif /* PLANCACHE_H */
diff --git a/src/pl/plpgsql/src/pl_exec.c b/src/pl/plpgsql/src/pl_exec.c
index 39ea925..9078bc1 100644
*** a/src/pl/plpgsql/src/pl_exec.c
--- b/src/pl/plpgsql/src/pl_exec.c
*************** typedef struct /* cast_hash table en
*** 152,157 ****
--- 152,158 ----
  {
  plpgsql_CastHashKey key; /* hash key --- MUST BE FIRST */
  Expr   *cast_expr; /* cast expression, or NULL if no-op cast */
+ CachedExpression *cast_cexpr; /* cached expression backing the above */
  /* ExprState is valid only when cast_lxid matches current LXID */
  ExprState  *cast_exprstate; /* expression's eval tree */
  bool cast_in_use; /* true while we're executing eval tree */
*************** get_cast_hashentry(PLpgSQL_execstate *es
*** 7610,7627 ****
  cast_key.dsttypmod = dsttypmod;
  cast_entry = (plpgsql_CastHashEntry *) hash_search(estate->cast_hash,
    (void *) &cast_key,
!   HASH_FIND, NULL);
 
! if (cast_entry == NULL)
  {
! /* We've not looked up this coercion before */
  Node   *cast_expr;
  CaseTestExpr *placeholder;
 
  /*
  * Since we could easily fail (no such coercion), construct a
  * temporary coercion expression tree in the short-lived
! * eval_mcontext, then if successful copy it to cast_hash_context.
  */
  oldcontext = MemoryContextSwitchTo(get_eval_mcontext(estate));
 
--- 7611,7644 ----
  cast_key.dsttypmod = dsttypmod;
  cast_entry = (plpgsql_CastHashEntry *) hash_search(estate->cast_hash,
    (void *) &cast_key,
!   HASH_ENTER, &found);
! if (!found) /* initialize if new entry */
! cast_entry->cast_cexpr = NULL;
 
! if (cast_entry->cast_cexpr == NULL ||
! !cast_entry->cast_cexpr->is_valid)
  {
! /*
! * We've not looked up this coercion before, or we have but the cached
! * expression has been invalidated.
! */
  Node   *cast_expr;
+ CachedExpression *cast_cexpr;
  CaseTestExpr *placeholder;
 
  /*
+ * Drop old cached expression if there is one.
+ */
+ if (cast_entry->cast_cexpr)
+ {
+ FreeCachedExpression(cast_entry->cast_cexpr);
+ cast_entry->cast_cexpr = NULL;
+ }
+
+ /*
  * Since we could easily fail (no such coercion), construct a
  * temporary coercion expression tree in the short-lived
! * eval_mcontext, then if successful save it as a CachedExpression.
  */
  oldcontext = MemoryContextSwitchTo(get_eval_mcontext(estate));
 
*************** get_cast_hashentry(PLpgSQL_execstate *es
*** 7682,7714 ****
 
  /* Note: we don't bother labeling the expression tree with collation */
 
  /* Detect whether we have a no-op (RelabelType) coercion */
  if (IsA(cast_expr, RelabelType) &&
  ((RelabelType *) cast_expr)->arg == (Expr *) placeholder)
  cast_expr = NULL;
 
! if (cast_expr)
! {
! /* ExecInitExpr assumes we've planned the expression */
! cast_expr = (Node *) expression_planner((Expr *) cast_expr);
!
! /* Now copy the tree into cast_hash_context */
! MemoryContextSwitchTo(estate->cast_hash_context);
!
! cast_expr = copyObject(cast_expr);
! }
!
! MemoryContextSwitchTo(oldcontext);
!
! /* Now we can fill in a hashtable entry. */
! cast_entry = (plpgsql_CastHashEntry *) hash_search(estate->cast_hash,
!   (void *) &cast_key,
!   HASH_ENTER, &found);
! Assert(!found); /* wasn't there a moment ago */
  cast_entry->cast_expr = (Expr *) cast_expr;
  cast_entry->cast_exprstate = NULL;
  cast_entry->cast_in_use = false;
  cast_entry->cast_lxid = InvalidLocalTransactionId;
  }
 
  /* Done if we have determined that this is a no-op cast. */
--- 7699,7721 ----
 
  /* Note: we don't bother labeling the expression tree with collation */
 
+ /* Plan the expression and build a CachedExpression */
+ cast_cexpr = GetCachedExpression(cast_expr);
+ cast_expr = cast_cexpr->expr;
+
  /* Detect whether we have a no-op (RelabelType) coercion */
  if (IsA(cast_expr, RelabelType) &&
  ((RelabelType *) cast_expr)->arg == (Expr *) placeholder)
  cast_expr = NULL;
 
! /* Now we can fill in the hashtable entry. */
! cast_entry->cast_cexpr = cast_cexpr;
  cast_entry->cast_expr = (Expr *) cast_expr;
  cast_entry->cast_exprstate = NULL;
  cast_entry->cast_in_use = false;
  cast_entry->cast_lxid = InvalidLocalTransactionId;
+
+ MemoryContextSwitchTo(oldcontext);
  }
 
  /* Done if we have determined that this is a no-op cast. */
diff --git a/src/test/regress/expected/domain.out b/src/test/regress/expected/domain.out
index 0b5a904..11bc772 100644
*** a/src/test/regress/expected/domain.out
--- b/src/test/regress/expected/domain.out
*************** create domain di as int;
*** 1020,1025 ****
--- 1020,1050 ----
  create function dom_check(int) returns di as $$
  declare d di;
  begin
+   d := $1::di;
+   return d;
+ end
+ $$ language plpgsql immutable;
+ select dom_check(0);
+  dom_check
+ -----------
+          0
+ (1 row)
+
+ alter domain di add constraint pos check (value > 0);
+ select dom_check(0); -- fail
+ ERROR:  value for domain di violates check constraint "pos"
+ CONTEXT:  PL/pgSQL function dom_check(integer) line 4 at assignment
+ alter domain di drop constraint pos;
+ select dom_check(0);
+  dom_check
+ -----------
+          0
+ (1 row)
+
+ -- implicit cast during assignment is a separate code path, test that too
+ create or replace function dom_check(int) returns di as $$
+ declare d di;
+ begin
    d := $1;
    return d;
  end
diff --git a/src/test/regress/sql/domain.sql b/src/test/regress/sql/domain.sql
index 68da27d..1291d55 100644
*** a/src/test/regress/sql/domain.sql
--- b/src/test/regress/sql/domain.sql
*************** create domain di as int;
*** 680,685 ****
--- 680,705 ----
  create function dom_check(int) returns di as $$
  declare d di;
  begin
+   d := $1::di;
+   return d;
+ end
+ $$ language plpgsql immutable;
+
+ select dom_check(0);
+
+ alter domain di add constraint pos check (value > 0);
+
+ select dom_check(0); -- fail
+
+ alter domain di drop constraint pos;
+
+ select dom_check(0);
+
+ -- implicit cast during assignment is a separate code path, test that too
+
+ create or replace function dom_check(int) returns di as $$
+ declare d di;
+ begin
    d := $1;
    return d;
  end