WIP patch for multiple column assignment in UPDATE - Mailing list pgsql-hackers

From Tom Lane
Subject WIP patch for multiple column assignment in UPDATE
Date
Msg-id 20930.1402931841@sss.pgh.pa.us
Whole thread Raw
Responses Re: WIP patch for multiple column assignment in UPDATE
List pgsql-hackers
Attached is a very-much-WIP patch for supporting
     UPDATE foo SET ..., (a,b,c) = (select x,y,z from ...), ...

It lacks documentation, ruleutils.c support, or any solution for the
rule NEW.* expansion problem I mentioned Saturday.  The reason I'm
posting it now is to get feedback about an implementation choice that
feels a bit klugy to me; but I don't see any clearly better idea.

The post-parse-analysis representation I've chosen here is that the
output columns of the sub-select are represented by PARAM_MULTIEXEC
Params, and the sub-select itself appears in a resjunk entry at the end
of the targetlist; that is, the UPDATE tlist is more or less like

   $1,  -- to be assigned to a
   $2,  -- to be assigned to b
   $3,  -- to be assigned to c
   (select x,y,z from ...),  -- resjunk entry, value will be discarded

If the sub-select is uncorrelated with the outer query, the planner
turns it into an InitPlan, replacing the resjunk tlist item with a
NULL constant, and then everything happens normally at execution.

But more usually, the sub-select will be correlated with the outer
query.  In this case, the subquery turns into a SubPlan tree that
stays in the resjunk item.  At the start of execution, the ParamExecData
structs for each of its output Params are marked with execPlan pointers
to the subplan, just as would happen for an InitPlan.  This causes the
subplan to get executed when the first of the output Params is evaluated;
it loads the ParamExecData structs for all its output Params, and then
the later Params just take data from the structs.  When execution reaches
the MULTIEXEC SubPlan in the resjunk tlist item, no evaluation of the
subplan is needed; but instead we re-mark all the output ParamExecData
structs with non-null execPlan pointers, so that a fresh execution of
the subplan will happen in the next evaluation of the targetlist.

The klugy aspect of this is that it assumes that the SubPlan item will
appear later in the tlist than all the Params referencing it.  This is
true at the moment because resjunk tlist items always appear after
non-resjunk ones.  There are a few other places that already depend on
this ordering, but we've mostly tried to avoid introducing new
dependencies on it.

The alternative that I'd originally had in mind, before put-it-in-a-
resjunk-item occurred to me, was to invent a new "secondary tlist"
field of Query and of ModifyTable plan nodes, as I sketched back in
http://www.postgresql.org/message-id/1783.1399054541@sss.pgh.pa.us
We'd put the MULTIEXEC SubPlans in this secondary tlist and expect
the executor to evaluate it just before evaluating the main tlist.
However, that way isn't terribly pretty either, because it extends
knowledge of this feature to a *whole lot* of places that don't have
to know about it in the attached approach; in particular, just about
every place that manipulates targetlist contents would have to also
manipulate the secondary tlist.

Another approach is to explicitly identify which of the Params will
be evaluated first and replace it with a node tree that evaluates
the subplan (and sets output Params for the remaining columns).
This is a bit messy because the first-to-be-evaluated is dependent
on the targetlist reordering that the planner does; so we don't want
parse analysis to try to do it.  (If we allowed parse analysis to
know that the planner will sort the tlist items into physical column
order, we could do it like that; but then it would break if we ever
get around to allowing ALTER TABLE to change the physical order.)
We could safely have setrefs.c determine the first-to-be-evaluated
Param, though, since it will traverse the tlist in final order.
So if we went with this approach I'd have setrefs.c replace the first
Param with a SubPlan node.  That seems a bit of a kluge too though.

Preferences, comments, better ideas?

            regards, tom lane

diff --git a/contrib/pg_stat_statements/pg_stat_statements.c b/contrib/pg_stat_statements/pg_stat_statements.c
index a3e8c59..7c2216a 100644
*** a/contrib/pg_stat_statements/pg_stat_statements.c
--- b/contrib/pg_stat_statements/pg_stat_statements.c
*************** JumbleExpr(pgssJumbleState *jstate, Node
*** 2396,2401 ****
--- 2396,2402 ----
                  SubLink    *sublink = (SubLink *) node;

                  APP_JUMB(sublink->subLinkType);
+                 APP_JUMB(sublink->subLinkId);
                  JumbleExpr(jstate, (Node *) sublink->testexpr);
                  JumbleQuery(jstate, (Query *) sublink->subselect);
              }
diff --git a/src/backend/executor/nodeSubplan.c b/src/backend/executor/nodeSubplan.c
index 5d02d94..4885ec0 100644
*** a/src/backend/executor/nodeSubplan.c
--- b/src/backend/executor/nodeSubplan.c
*************** ExecSubPlan(SubPlanState *node,
*** 72,78 ****
      /* Sanity checks */
      if (subplan->subLinkType == CTE_SUBLINK)
          elog(ERROR, "CTE subplans should not be executed via ExecSubPlan");
!     if (subplan->setParam != NIL)
          elog(ERROR, "cannot set parent params from subquery");

      /* Select appropriate evaluation strategy */
--- 72,78 ----
      /* Sanity checks */
      if (subplan->subLinkType == CTE_SUBLINK)
          elog(ERROR, "CTE subplans should not be executed via ExecSubPlan");
!     if (subplan->setParam != NIL && subplan->subLinkType != MULTIEXPR_SUBLINK)
          elog(ERROR, "cannot set parent params from subquery");

      /* Select appropriate evaluation strategy */
*************** ExecScanSubPlan(SubPlanState *node,
*** 231,236 ****
--- 231,259 ----
      oldcontext = MemoryContextSwitchTo(econtext->ecxt_per_query_memory);

      /*
+      * MULTIEXPR subplans, when "executed", just return NULL; but first we
+      * mark the subplan's output parameters as needing recalculation.  XXX
+      * this is a bit of a hack.  Note that unlike ExecReScanSetParamPlan, we
+      * do *not* set bits in the parent's chgParam, because we don't want to
+      * cause a rescan of the parent.
+      */
+     if (subLinkType == MULTIEXPR_SUBLINK)
+     {
+         EState       *estate = node->parent->state;
+
+         foreach(l, subplan->setParam)
+         {
+             int            paramid = lfirst_int(l);
+             ParamExecData *prm = &(estate->es_param_exec_vals[paramid]);
+
+             prm->execPlan = node;
+         }
+         MemoryContextSwitchTo(oldcontext);
+         *isNull = true;
+         return (Datum) 0;
+     }
+
+     /*
       * Set Params of this plan from parent plan correlation values. (Any
       * calculation we have to do is done in the parent econtext, since the
       * Param values don't need to have per-query lifetime.)
*************** ExecInitSubPlan(SubPlan *subplan, PlanSt
*** 667,672 ****
--- 690,698 ----
      sstate->planstate = (PlanState *) list_nth(estate->es_subplanstates,
                                                 subplan->plan_id - 1);

+     /* ... and to its parent's state */
+     sstate->parent = parent;
+
      /* Initialize subexpressions */
      sstate->testexpr = ExecInitExpr((Expr *) subplan->testexpr, parent);
      sstate->args = (List *) ExecInitExpr((Expr *) subplan->args, parent);
*************** ExecInitSubPlan(SubPlan *subplan, PlanSt
*** 690,695 ****
--- 716,723 ----
      sstate->cur_eq_funcs = NULL;

      /*
+      * XXX comment needs update
+      *
       * If this plan is un-correlated or undirect correlated one and want to
       * set params for parent plan then mark parameters as needing evaluation.
       *
*************** ExecInitSubPlan(SubPlan *subplan, PlanSt
*** 890,896 ****
  /* ----------------------------------------------------------------
   *        ExecSetParamPlan
   *
!  *        Executes an InitPlan subplan and sets its output parameters.
   *
   * This is called from ExecEvalParamExec() when the value of a PARAM_EXEC
   * parameter is requested and the param's execPlan field is set (indicating
--- 918,924 ----
  /* ----------------------------------------------------------------
   *        ExecSetParamPlan
   *
!  *        Executes a subplan and sets its output parameters.
   *
   * This is called from ExecEvalParamExec() when the value of a PARAM_EXEC
   * parameter is requested and the param's execPlan field is set (indicating
*************** ExecSetParamPlan(SubPlanState *node, Exp
*** 908,913 ****
--- 936,942 ----
      SubLinkType subLinkType = subplan->subLinkType;
      MemoryContext oldcontext;
      TupleTableSlot *slot;
+     ListCell   *pvar;
      ListCell   *l;
      bool        found = false;
      ArrayBuildState *astate = NULL;
*************** ExecSetParamPlan(SubPlanState *node, Exp
*** 924,929 ****
--- 953,979 ----
      oldcontext = MemoryContextSwitchTo(econtext->ecxt_per_query_memory);

      /*
+      * Set Params of this plan from parent plan correlation values. (Any
+      * calculation we have to do is done in the parent econtext, since the
+      * Param values don't need to have per-query lifetime.)  Currently, we
+      * expect only MULTIEXPR_SUBLINK plans to have any correlation values.
+      */
+     Assert(subplan->parParam == NIL || subLinkType == MULTIEXPR_SUBLINK);
+     Assert(list_length(subplan->parParam) == list_length(node->args));
+
+     forboth(l, subplan->parParam, pvar, node->args)
+     {
+         int            paramid = lfirst_int(l);
+         ParamExecData *prm = &(econtext->ecxt_param_exec_vals[paramid]);
+
+         prm->value = ExecEvalExprSwitchContext((ExprState *) lfirst(pvar),
+                                                econtext,
+                                                &(prm->isnull),
+                                                NULL);
+         planstate->chgParam = bms_add_member(planstate->chgParam, paramid);
+     }
+
+     /*
       * Run the plan.  (If it needs to be rescanned, the first ExecProcNode
       * call will take care of that.)
       */
*************** ExecSetParamPlan(SubPlanState *node, Exp
*** 964,969 ****
--- 1014,1020 ----

          if (found &&
              (subLinkType == EXPR_SUBLINK ||
+              subLinkType == MULTIEXPR_SUBLINK ||
               subLinkType == ROWCOMPARE_SUBLINK))
              ereport(ERROR,
                      (errcode(ERRCODE_CARDINALITY_VIOLATION),
diff --git a/src/backend/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c
index 43530aa..8d3d5a7 100644
*** a/src/backend/nodes/copyfuncs.c
--- b/src/backend/nodes/copyfuncs.c
*************** _copySubLink(const SubLink *from)
*** 1327,1332 ****
--- 1327,1333 ----
      SubLink    *newnode = makeNode(SubLink);

      COPY_SCALAR_FIELD(subLinkType);
+     COPY_SCALAR_FIELD(subLinkId);
      COPY_NODE_FIELD(testexpr);
      COPY_NODE_FIELD(operName);
      COPY_NODE_FIELD(subselect);
*************** _copyResTarget(const ResTarget *from)
*** 2247,2252 ****
--- 2248,2265 ----
      return newnode;
  }

+ static MultiAssignRef *
+ _copyMultiAssignRef(const MultiAssignRef *from)
+ {
+     MultiAssignRef *newnode = makeNode(MultiAssignRef);
+
+     COPY_NODE_FIELD(source);
+     COPY_SCALAR_FIELD(colno);
+     COPY_SCALAR_FIELD(ncolumns);
+
+     return newnode;
+ }
+
  static TypeName *
  _copyTypeName(const TypeName *from)
  {
*************** copyObject(const void *from)
*** 4561,4566 ****
--- 4574,4582 ----
          case T_ResTarget:
              retval = _copyResTarget(from);
              break;
+         case T_MultiAssignRef:
+             retval = _copyMultiAssignRef(from);
+             break;
          case T_TypeCast:
              retval = _copyTypeCast(from);
              break;
diff --git a/src/backend/nodes/equalfuncs.c b/src/backend/nodes/equalfuncs.c
index 2407cb7..e7b49f6 100644
*** a/src/backend/nodes/equalfuncs.c
--- b/src/backend/nodes/equalfuncs.c
*************** static bool
*** 382,387 ****
--- 382,388 ----
  _equalSubLink(const SubLink *a, const SubLink *b)
  {
      COMPARE_SCALAR_FIELD(subLinkType);
+     COMPARE_SCALAR_FIELD(subLinkId);
      COMPARE_NODE_FIELD(testexpr);
      COMPARE_NODE_FIELD(operName);
      COMPARE_NODE_FIELD(subselect);
*************** _equalResTarget(const ResTarget *a, cons
*** 2095,2100 ****
--- 2096,2111 ----
  }

  static bool
+ _equalMultiAssignRef(const MultiAssignRef *a, const MultiAssignRef *b)
+ {
+     COMPARE_NODE_FIELD(source);
+     COMPARE_SCALAR_FIELD(colno);
+     COMPARE_SCALAR_FIELD(ncolumns);
+
+     return true;
+ }
+
+ static bool
  _equalTypeName(const TypeName *a, const TypeName *b)
  {
      COMPARE_NODE_FIELD(names);
*************** equal(const void *a, const void *b)
*** 3029,3034 ****
--- 3040,3048 ----
          case T_ResTarget:
              retval = _equalResTarget(a, b);
              break;
+         case T_MultiAssignRef:
+             retval = _equalMultiAssignRef(a, b);
+             break;
          case T_TypeCast:
              retval = _equalTypeCast(a, b);
              break;
diff --git a/src/backend/nodes/list.c b/src/backend/nodes/list.c
index f32124b..5c09d2f 100644
*** a/src/backend/nodes/list.c
--- b/src/backend/nodes/list.c
*************** list_truncate(List *list, int new_size)
*** 385,391 ****
   * Locate the n'th cell (counting from 0) of the list.  It is an assertion
   * failure if there is no such cell.
   */
! static ListCell *
  list_nth_cell(const List *list, int n)
  {
      ListCell   *match;
--- 385,391 ----
   * Locate the n'th cell (counting from 0) of the list.  It is an assertion
   * failure if there is no such cell.
   */
! ListCell *
  list_nth_cell(const List *list, int n)
  {
      ListCell   *match;
diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c
index 5a98bfb..546aaef 100644
*** a/src/backend/nodes/nodeFuncs.c
--- b/src/backend/nodes/nodeFuncs.c
*************** exprType(const Node *expr)
*** 116,121 ****
--- 116,129 ----
                              format_type_be(exprType((Node *) tent->expr)))));
                      }
                  }
+                 else if (sublink->subLinkType == MULTIEXPR_SUBLINK)
+                 {
+                     /*
+                      * We probably shouldn't ever be asked the type of a
+                      * MULTIEXPR subquery, but if we are, say RECORD
+                      */
+                     type = RECORDOID;
+                 }
                  else
                  {
                      /* for all other sublink types, result is boolean */
*************** exprType(const Node *expr)
*** 142,147 ****
--- 150,163 ----
                                      format_type_be(subplan->firstColType))));
                      }
                  }
+                 else if (subplan->subLinkType == MULTIEXPR_SUBLINK)
+                 {
+                     /*
+                      * We probably shouldn't ever be asked the type of a
+                      * MULTIEXPR subquery, but if we are, say RECORD
+                      */
+                     type = RECORDOID;
+                 }
                  else
                  {
                      /* for all other subplan types, result is boolean */
*************** exprTypmod(const Node *expr)
*** 299,304 ****
--- 315,321 ----
                      return exprTypmod((Node *) tent->expr);
                      /* note we don't need to care if it's an array */
                  }
+                 /* otherwise, result is RECORD or BOOLEAN, typmod is -1 */
              }
              break;
          case T_SubPlan:
*************** exprTypmod(const Node *expr)
*** 312,322 ****
                      /* note we don't need to care if it's an array */
                      return subplan->firstColTypmod;
                  }
!                 else
!                 {
!                     /* for all other subplan types, result is boolean */
!                     return -1;
!                 }
              }
              break;
          case T_AlternativeSubPlan:
--- 329,335 ----
                      /* note we don't need to care if it's an array */
                      return subplan->firstColTypmod;
                  }
!                 /* otherwise, result is RECORD or BOOLEAN, typmod is -1 */
              }
              break;
          case T_AlternativeSubPlan:
*************** exprCollation(const Node *expr)
*** 784,790 ****
                  }
                  else
                  {
!                     /* for all other sublink types, result is boolean */
                      coll = InvalidOid;
                  }
              }
--- 797,803 ----
                  }
                  else
                  {
!                     /* otherwise, result is RECORD or BOOLEAN */
                      coll = InvalidOid;
                  }
              }
*************** exprCollation(const Node *expr)
*** 802,808 ****
                  }
                  else
                  {
!                     /* for all other subplan types, result is boolean */
                      coll = InvalidOid;
                  }
              }
--- 815,821 ----
                  }
                  else
                  {
!                     /* otherwise, result is RECORD or BOOLEAN */
                      coll = InvalidOid;
                  }
              }
*************** exprSetCollation(Node *expr, Oid collati
*** 1017,1023 ****
                  }
                  else
                  {
!                     /* for all other sublink types, result is boolean */
                      Assert(!OidIsValid(collation));
                  }
              }
--- 1030,1036 ----
                  }
                  else
                  {
!                     /* otherwise, result is RECORD or BOOLEAN */
                      Assert(!OidIsValid(collation));
                  }
              }
*************** exprLocation(const Node *expr)
*** 1420,1425 ****
--- 1433,1441 ----
              /* we need not examine the contained expression (if any) */
              loc = ((const ResTarget *) expr)->location;
              break;
+         case T_MultiAssignRef:
+             loc = exprLocation(((const MultiAssignRef *) expr)->source);
+             break;
          case T_TypeCast:
              {
                  const TypeCast *tc = (const TypeCast *) expr;
*************** raw_expression_tree_walker(Node *node,
*** 3099,3104 ****
--- 3115,3122 ----
                      return true;
              }
              break;
+         case T_MultiAssignRef:
+             return walker(((MultiAssignRef *) node)->source, context);
          case T_TypeCast:
              {
                  TypeCast   *tc = (TypeCast *) node;
diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c
index 11c7486..d25641f 100644
*** a/src/backend/nodes/outfuncs.c
--- b/src/backend/nodes/outfuncs.c
*************** _outSubLink(StringInfo str, const SubLin
*** 1115,1120 ****
--- 1115,1121 ----
      WRITE_NODE_TYPE("SUBLINK");

      WRITE_ENUM_FIELD(subLinkType, SubLinkType);
+     WRITE_INT_FIELD(subLinkId);
      WRITE_NODE_FIELD(testexpr);
      WRITE_NODE_FIELD(operName);
      WRITE_NODE_FIELD(subselect);
*************** _outPlannerInfo(StringInfo str, const Pl
*** 1701,1706 ****
--- 1702,1708 ----
      WRITE_INT_FIELD(join_cur_level);
      WRITE_NODE_FIELD(init_plans);
      WRITE_NODE_FIELD(cte_plan_ids);
+     WRITE_NODE_FIELD(multiexpr_params);
      WRITE_NODE_FIELD(eq_classes);
      WRITE_NODE_FIELD(canon_pathkeys);
      WRITE_NODE_FIELD(left_join_clauses);
*************** _outResTarget(StringInfo str, const ResT
*** 2590,2595 ****
--- 2592,2607 ----
  }

  static void
+ _outMultiAssignRef(StringInfo str, const MultiAssignRef *node)
+ {
+     WRITE_NODE_TYPE("MULTIASSIGNREF");
+
+     WRITE_NODE_FIELD(source);
+     WRITE_INT_FIELD(colno);
+     WRITE_INT_FIELD(ncolumns);
+ }
+
+ static void
  _outSortBy(StringInfo str, const SortBy *node)
  {
      WRITE_NODE_TYPE("SORTBY");
*************** _outNode(StringInfo str, const void *obj
*** 3200,3205 ****
--- 3212,3220 ----
              case T_ResTarget:
                  _outResTarget(str, obj);
                  break;
+             case T_MultiAssignRef:
+                 _outMultiAssignRef(str, obj);
+                 break;
              case T_SortBy:
                  _outSortBy(str, obj);
                  break;
diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c
index 1ec4f3c..69d9989 100644
*** a/src/backend/nodes/readfuncs.c
--- b/src/backend/nodes/readfuncs.c
*************** _readSubLink(void)
*** 744,749 ****
--- 744,750 ----
      READ_LOCALS(SubLink);

      READ_ENUM_FIELD(subLinkType, SubLinkType);
+     READ_INT_FIELD(subLinkId);
      READ_NODE_FIELD(testexpr);
      READ_NODE_FIELD(operName);
      READ_NODE_FIELD(subselect);
diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c
index 0f1e2e4..f2c9c99 100644
*** a/src/backend/optimizer/plan/planner.c
--- b/src/backend/optimizer/plan/planner.c
*************** subquery_planner(PlannerGlobal *glob, Qu
*** 310,315 ****
--- 310,316 ----
      root->planner_cxt = CurrentMemoryContext;
      root->init_plans = NIL;
      root->cte_plan_ids = NIL;
+     root->multiexpr_params = NIL;
      root->eq_classes = NIL;
      root->append_rel_list = NIL;
      root->rowMarks = NIL;
diff --git a/src/backend/optimizer/plan/setrefs.c b/src/backend/optimizer/plan/setrefs.c
index 768c5c7..4d717df 100644
*** a/src/backend/optimizer/plan/setrefs.c
--- b/src/backend/optimizer/plan/setrefs.c
*************** static bool extract_query_dependencies_w
*** 157,166 ****
   * 3. We adjust Vars in upper plan nodes to refer to the outputs of their
   * subplans.
   *
!  * 4. We compute regproc OIDs for operators (ie, we look up the function
   * that implements each op).
   *
!  * 5. We create lists of specific objects that the plan depends on.
   * 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).
--- 157,169 ----
   * 3. We adjust Vars in upper plan nodes to refer to the outputs of their
   * subplans.
   *
!  * 4. PARAM_MULTIEXPR Params are replaced by regular PARAM_EXEC Params,
!  * now that we have finished planning all MULTIEXPR subplans.
!  *
!  * 5. We compute regproc OIDs for operators (ie, we look up the function
   * that implements each op).
   *
!  * 6. We create lists of specific objects that the plan depends on.
   * 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).
*************** fix_expr_common(PlannerInfo *root, Node
*** 1119,1128 ****
--- 1122,1160 ----
  }

  /*
+  * fix_param_node
+  *        Do set_plan_references processing on a Param
+  *
+  * If it's a PARAM_MULTIEXPR, replace it with the appropriate Param from
+  * root->multiexpr_params; otherwise no change is needed.
+  * Just for paranoia's sake, we make a copy of the node in either case.
+  */
+ static Node *
+ fix_param_node(PlannerInfo *root, Param *p)
+ {
+     if (p->paramkind == PARAM_MULTIEXPR)
+     {
+         int            subqueryid = p->paramid >> 16;
+         int            colno = p->paramid & 0xFFFF;
+         List       *params;
+
+         if (subqueryid <= 0 ||
+             subqueryid > list_length(root->multiexpr_params))
+             elog(ERROR, "unexpected PARAM_MULTIEXPR ID: %d", p->paramid);
+         params = (List *) list_nth(root->multiexpr_params, subqueryid - 1);
+         if (colno <= 0 || colno > list_length(params))
+             elog(ERROR, "unexpected PARAM_MULTIEXPR ID: %d", p->paramid);
+         return copyObject(list_nth(params, colno - 1));
+     }
+     return copyObject(p);
+ }
+
+ /*
   * fix_scan_expr
   *        Do set_plan_references processing on a scan-level expression
   *
   * This consists of incrementing all Vars' varnos by rtoffset,
+  * replacing PARAM_MULTIEXPR Params, expanding PlaceHolderVars,
   * looking up operator opcode info for OpExpr and related nodes,
   * and adding OIDs from regclass Const nodes into root->glob->relationOids.
   */
*************** fix_scan_expr(PlannerInfo *root, Node *n
*** 1134,1140 ****
      context.root = root;
      context.rtoffset = rtoffset;

!     if (rtoffset != 0 || root->glob->lastPHId != 0)
      {
          return fix_scan_expr_mutator(node, &context);
      }
--- 1166,1174 ----
      context.root = root;
      context.rtoffset = rtoffset;

!     if (rtoffset != 0 ||
!         root->multiexpr_params != NIL ||
!         root->glob->lastPHId != 0)
      {
          return fix_scan_expr_mutator(node, &context);
      }
*************** fix_scan_expr(PlannerInfo *root, Node *n
*** 1142,1152 ****
      {
          /*
           * If rtoffset == 0, we don't need to change any Vars, and if there
!          * are no placeholders anywhere we won't need to remove them.  Then
!          * it's OK to just scribble on the input node tree instead of copying
!          * (since the only change, filling in any unset opfuncid fields, is
!          * harmless).  This saves just enough cycles to be noticeable on
!          * trivial queries.
           */
          (void) fix_scan_expr_walker(node, &context);
          return node;
--- 1176,1187 ----
      {
          /*
           * If rtoffset == 0, we don't need to change any Vars, and if there
!          * are no MULTIEXPR subqueries then we don't need to replace
!          * PARAM_MULTIEXPR Params, and if there are no placeholders anywhere
!          * we won't need to remove them.  Then it's OK to just scribble on the
!          * input node tree instead of copying (since the only change, filling
!          * in any unset opfuncid fields, is harmless).  This saves just enough
!          * cycles to be noticeable on trivial queries.
           */
          (void) fix_scan_expr_walker(node, &context);
          return node;
*************** fix_scan_expr_mutator(Node *node, fix_sc
*** 1176,1181 ****
--- 1211,1218 ----
              var->varnoold += context->rtoffset;
          return (Node *) var;
      }
+     if (IsA(node, Param))
+         return fix_param_node(context->root, (Param *) node);
      if (IsA(node, CurrentOfExpr))
      {
          CurrentOfExpr *cexpr = (CurrentOfExpr *) copyObject(node);
*************** fix_join_expr_mutator(Node *node, fix_jo
*** 1745,1750 ****
--- 1782,1789 ----
          /* If not supplied by input plans, evaluate the contained expr */
          return fix_join_expr_mutator((Node *) phv->phexpr, context);
      }
+     if (IsA(node, Param))
+         return fix_param_node(context->root, (Param *) node);
      /* Try matching more complex expressions too, if tlists have any */
      if (context->outer_itlist->has_non_vars)
      {
*************** fix_upper_expr_mutator(Node *node, fix_u
*** 1847,1852 ****
--- 1886,1893 ----
          /* If not supplied by input plan, evaluate the contained expr */
          return fix_upper_expr_mutator((Node *) phv->phexpr, context);
      }
+     if (IsA(node, Param))
+         return fix_param_node(context->root, (Param *) node);
      /* Try matching more complex expressions too, if tlist has any */
      if (context->subplan_itlist->has_non_vars)
      {
diff --git a/src/backend/optimizer/plan/subselect.c b/src/backend/optimizer/plan/subselect.c
index be92049..4694ec4 100644
*** a/src/backend/optimizer/plan/subselect.c
--- b/src/backend/optimizer/plan/subselect.c
*************** typedef struct finalize_primnode_context
*** 55,62 ****

  static Node *build_subplan(PlannerInfo *root, Plan *plan, PlannerInfo *subroot,
                List *plan_params,
!               SubLinkType subLinkType, Node *testexpr,
!               bool adjust_testexpr, bool unknownEqFalse);
  static List *generate_subquery_params(PlannerInfo *root, List *tlist,
                           List **paramIds);
  static List *generate_subquery_vars(PlannerInfo *root, List *tlist,
--- 55,63 ----

  static Node *build_subplan(PlannerInfo *root, Plan *plan, PlannerInfo *subroot,
                List *plan_params,
!               SubLinkType subLinkType, int subLinkId,
!               Node *testexpr, bool adjust_testexpr,
!               bool unknownEqFalse);
  static List *generate_subquery_params(PlannerInfo *root, List *tlist,
                           List **paramIds);
  static List *generate_subquery_vars(PlannerInfo *root, List *tlist,
*************** get_first_col_type(Plan *plan, Oid *colt
*** 407,413 ****
  /*
   * Convert a SubLink (as created by the parser) into a SubPlan.
   *
!  * We are given the SubLink's contained query, type, and testexpr.  We are
   * also told if this expression appears at top level of a WHERE/HAVING qual.
   *
   * Note: we assume that the testexpr has been AND/OR flattened (actually,
--- 408,414 ----
  /*
   * Convert a SubLink (as created by the parser) into a SubPlan.
   *
!  * We are given the SubLink's contained query, type, ID, and testexpr.  We are
   * also told if this expression appears at top level of a WHERE/HAVING qual.
   *
   * Note: we assume that the testexpr has been AND/OR flattened (actually,
*************** get_first_col_type(Plan *plan, Oid *colt
*** 419,428 ****
   * node in the executable expression.  This will be either the SubPlan
   * node (if we have to do the subplan as a subplan), or a Param node
   * representing the result of an InitPlan, or a row comparison expression
!  * tree containing InitPlan Param nodes.
   */
  static Node *
! make_subplan(PlannerInfo *root, Query *orig_subquery, SubLinkType subLinkType,
               Node *testexpr, bool isTopQual)
  {
      Query       *subquery;
--- 420,430 ----
   * node in the executable expression.  This will be either the SubPlan
   * node (if we have to do the subplan as a subplan), or a Param node
   * representing the result of an InitPlan, or a row comparison expression
!  * tree containing InitPlan Param nodes.  XXX describe MULTIEXPR case
   */
  static Node *
! make_subplan(PlannerInfo *root, Query *orig_subquery,
!              SubLinkType subLinkType, int subLinkId,
               Node *testexpr, bool isTopQual)
  {
      Query       *subquery;
*************** make_subplan(PlannerInfo *root, Query *o
*** 452,459 ****
       * first tuple will be retrieved.  For ALL and ANY subplans, we will be
       * able to stop evaluating if the test condition fails or matches, so very
       * often not all the tuples will be retrieved; for lack of a better idea,
!      * specify 50% retrieval.  For EXPR and ROWCOMPARE subplans, use default
!      * behavior (we're only expecting one row out, anyway).
       *
       * NOTE: if you change these numbers, also change cost_subplan() in
       * path/costsize.c.
--- 454,461 ----
       * first tuple will be retrieved.  For ALL and ANY subplans, we will be
       * able to stop evaluating if the test condition fails or matches, so very
       * often not all the tuples will be retrieved; for lack of a better idea,
!      * specify 50% retrieval.  For EXPR, MULTIEXPR, and ROWCOMPARE subplans,
!      * use default behavior (we're only expecting one row out, anyway).
       *
       * NOTE: if you change these numbers, also change cost_subplan() in
       * path/costsize.c.
*************** make_subplan(PlannerInfo *root, Query *o
*** 491,497 ****

      /* And convert to SubPlan or InitPlan format. */
      result = build_subplan(root, plan, subroot, plan_params,
!                            subLinkType, testexpr, true, isTopQual);

      /*
       * If it's a correlated EXISTS with an unimportant targetlist, we might be
--- 493,500 ----

      /* And convert to SubPlan or InitPlan format. */
      result = build_subplan(root, plan, subroot, plan_params,
!                            subLinkType, subLinkId,
!                            testexpr, true, isTopQual);

      /*
       * If it's a correlated EXISTS with an unimportant targetlist, we might be
*************** make_subplan(PlannerInfo *root, Query *o
*** 536,542 ****
                  /* OK, convert to SubPlan format. */
                  hashplan = (SubPlan *) build_subplan(root, plan, subroot,
                                                       plan_params,
!                                                      ANY_SUBLINK, newtestexpr,
                                                       false, true);
                  /* Check we got what we expected */
                  Assert(IsA(hashplan, SubPlan));
--- 539,546 ----
                  /* OK, convert to SubPlan format. */
                  hashplan = (SubPlan *) build_subplan(root, plan, subroot,
                                                       plan_params,
!                                                      ANY_SUBLINK, 0,
!                                                      newtestexpr,
                                                       false, true);
                  /* Check we got what we expected */
                  Assert(IsA(hashplan, SubPlan));
*************** make_subplan(PlannerInfo *root, Query *o
*** 560,572 ****
   * Build a SubPlan node given the raw inputs --- subroutine for make_subplan
   *
   * Returns either the SubPlan, or an expression using initplan output Params,
!  * as explained in the comments for make_subplan.
   */
  static Node *
  build_subplan(PlannerInfo *root, Plan *plan, PlannerInfo *subroot,
                List *plan_params,
!               SubLinkType subLinkType, Node *testexpr,
!               bool adjust_testexpr, bool unknownEqFalse)
  {
      Node       *result;
      SubPlan    *splan;
--- 564,577 ----
   * Build a SubPlan node given the raw inputs --- subroutine for make_subplan
   *
   * Returns either the SubPlan, or an expression using initplan output Params,
!  * as explained in the comments for make_subplan. XXX more commentary
   */
  static Node *
  build_subplan(PlannerInfo *root, Plan *plan, PlannerInfo *subroot,
                List *plan_params,
!               SubLinkType subLinkType, int subLinkId,
!               Node *testexpr, bool adjust_testexpr,
!               bool unknownEqFalse)
  {
      Node       *result;
      SubPlan    *splan;
*************** build_subplan(PlannerInfo *root, Plan *p
*** 615,626 ****
      }

      /*
!      * Un-correlated or undirect correlated plans of EXISTS, EXPR, ARRAY, or
!      * ROWCOMPARE types can be used as initPlans.  For EXISTS, EXPR, or ARRAY,
!      * we just produce a Param referring to the result of evaluating the
!      * initPlan.  For ROWCOMPARE, we must modify the testexpr tree to contain
!      * PARAM_EXEC Params instead of the PARAM_SUBLINK Params emitted by the
!      * parser.
       */
      if (splan->parParam == NIL && subLinkType == EXISTS_SUBLINK)
      {
--- 620,634 ----
      }

      /*
!      * Un-correlated or undirect correlated plans of EXISTS, EXPR, MULTIEXPR,
!      * ARRAY, or ROWCOMPARE types can be used as initPlans.  For EXISTS, EXPR,
!      * or ARRAY, we return a Param referring to the result of evaluating the
!      * initPlan.  For MULTIEXPR, we return a null constant: the resjunk
!      * targetlist item containing the SubLink does not need to return anything
!      * useful, since the referencing Params are elsewhere.  For ROWCOMPARE, we
!      * must modify the testexpr tree to contain PARAM_EXEC Params instead of
!      * the PARAM_SUBLINK Params emitted by the parser, and then return that
!      * tree.
       */
      if (splan->parParam == NIL && subLinkType == EXISTS_SUBLINK)
      {
*************** build_subplan(PlannerInfo *root, Plan *p
*** 687,692 ****
--- 695,736 ----
           * plan's expression tree; it is not kept in the initplan node.
           */
      }
+     else if (subLinkType == MULTIEXPR_SUBLINK)
+     {
+         /*
+          * Whether it's an initplan or not, it needs to set a PARAM_EXEC Param
+          * for each output column.
+          */
+         List       *params;
+
+         Assert(testexpr == NULL);
+         params = generate_subquery_params(root,
+                                           plan->targetlist,
+                                           &splan->setParam);
+
+         /*
+          * Save the list of replacement Params in the n'th cell of
+          * root->multiexpr_params; setrefs.c will use it to replace
+          * PARAM_MULTIEXPR Params.
+          */
+         while (list_length(root->multiexpr_params) < subLinkId)
+             root->multiexpr_params = lappend(root->multiexpr_params, NIL);
+         lc = list_nth_cell(root->multiexpr_params, subLinkId - 1);
+         Assert(lfirst(lc) == NIL);
+         lfirst(lc) = params;
+
+         /* It can be an initplan if there are no parParams. */
+         if (splan->parParam == NIL)
+         {
+             isInitPlan = true;
+             result = (Node *) makeNullConst(RECORDOID, -1, InvalidOid);
+         }
+         else
+         {
+             isInitPlan = false;
+             result = (Node *) splan;
+         }
+     }
      else
      {
          /*
*************** process_sublinks_mutator(Node *node, pro
*** 1816,1821 ****
--- 1860,1866 ----
          return make_subplan(context->root,
                              (Query *) sublink->subselect,
                              sublink->subLinkType,
+                             sublink->subLinkId,
                              testexpr,
                              context->isTopQual);
      }
diff --git a/src/backend/optimizer/prep/prepjointree.c b/src/backend/optimizer/prep/prepjointree.c
index 776fe42..1030c78 100644
*** a/src/backend/optimizer/prep/prepjointree.c
--- b/src/backend/optimizer/prep/prepjointree.c
*************** pull_up_simple_subquery(PlannerInfo *roo
*** 804,809 ****
--- 804,810 ----
      subroot->planner_cxt = CurrentMemoryContext;
      subroot->init_plans = NIL;
      subroot->cte_plan_ids = NIL;
+     subroot->multiexpr_params = NIL;
      subroot->eq_classes = NIL;
      subroot->append_rel_list = NIL;
      subroot->rowMarks = NIL;
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 7b9895d..5b33818 100644
*** a/src/backend/parser/gram.y
--- b/src/backend/parser/gram.y
*************** single_set_clause:
*** 9231,9236 ****
--- 9231,9244 ----
                  }
          ;

+ /*
+  * Ideally, we'd accept any row-valued a_expr as RHS of a multiple_set_clause.
+  * However, per SQL spec the row-constructor case must allow DEFAULT as a row
+  * member, and it's pretty unclear how to do that (unless perhaps we allow
+  * DEFAULT in any a_expr and let parse analysis sort it out later?).  For the
+  * moment, the planner/executor only support a subquery as a MultiAssignment
+  * source anyhow, so we need only accept ctext_row and subqueries here.
+  */
  multiple_set_clause:
              '(' set_target_list ')' '=' ctext_row
                  {
*************** multiple_set_clause:
*** 9239,9252 ****

                      /*
                       * Break the ctext_row apart, merge individual expressions
!                      * into the destination ResTargets.  XXX this approach
!                      * cannot work for general row expressions as sources.
                       */
                      if (list_length($2) != list_length($5))
                          ereport(ERROR,
                                  (errcode(ERRCODE_SYNTAX_ERROR),
                                   errmsg("number of columns does not match number of values"),
!                                  parser_errposition(@1)));
                      forboth(col_cell, $2, val_cell, $5)
                      {
                          ResTarget *res_col = (ResTarget *) lfirst(col_cell);
--- 9247,9261 ----

                      /*
                       * Break the ctext_row apart, merge individual expressions
!                      * into the destination ResTargets.  This is semantically
!                      * equivalent to, and much cheaper to process than, the
!                      * general case.
                       */
                      if (list_length($2) != list_length($5))
                          ereport(ERROR,
                                  (errcode(ERRCODE_SYNTAX_ERROR),
                                   errmsg("number of columns does not match number of values"),
!                                  parser_errposition(@5)));
                      forboth(col_cell, $2, val_cell, $5)
                      {
                          ResTarget *res_col = (ResTarget *) lfirst(col_cell);
*************** multiple_set_clause:
*** 9257,9262 ****
--- 9266,9301 ----

                      $$ = $2;
                  }
+             | '(' set_target_list ')' '=' select_with_parens
+                 {
+                     SubLink *sl = makeNode(SubLink);
+                     int ncolumns = list_length($2);
+                     int i = 1;
+                     ListCell *col_cell;
+
+                     /* First, convert bare SelectStmt into a SubLink */
+                     sl->subLinkType = MULTIEXPR_SUBLINK;
+                     sl->subLinkId = 0;        /* will be assigned later */
+                     sl->testexpr = NULL;
+                     sl->operName = NIL;
+                     sl->subselect = $5;
+                     sl->location = @5;
+
+                     /* Create a MultiAssignRef source for each target */
+                     foreach(col_cell, $2)
+                     {
+                         ResTarget *res_col = (ResTarget *) lfirst(col_cell);
+                         MultiAssignRef *r = makeNode(MultiAssignRef);
+
+                         r->source = (Node *) sl;
+                         r->colno = i;
+                         r->ncolumns = ncolumns;
+                         res_col->val = (Node *) r;
+                         i++;
+                     }
+
+                     $$ = $2;
+                 }
          ;

  set_target:
*************** a_expr:        c_expr                                    { $$ = $1; }
*** 11090,11095 ****
--- 11129,11135 ----
                          /* generate foo = ANY (subquery) */
                          SubLink *n = (SubLink *) $3;
                          n->subLinkType = ANY_SUBLINK;
+                         n->subLinkId = 0;
                          n->testexpr = $1;
                          n->operName = list_make1(makeString("="));
                          n->location = @2;
*************** a_expr:        c_expr                                    { $$ = $1; }
*** 11110,11115 ****
--- 11150,11156 ----
                          /* Make an = ANY node */
                          SubLink *n = (SubLink *) $4;
                          n->subLinkType = ANY_SUBLINK;
+                         n->subLinkId = 0;
                          n->testexpr = $1;
                          n->operName = list_make1(makeString("="));
                          n->location = @3;
*************** a_expr:        c_expr                                    { $$ = $1; }
*** 11126,11131 ****
--- 11167,11173 ----
                  {
                      SubLink *n = makeNode(SubLink);
                      n->subLinkType = $3;
+                     n->subLinkId = 0;
                      n->testexpr = $1;
                      n->operName = $2;
                      n->subselect = $4;
*************** c_expr:        columnref                                { $$ = $1; }
*** 11286,11291 ****
--- 11328,11334 ----
                  {
                      SubLink *n = makeNode(SubLink);
                      n->subLinkType = EXPR_SUBLINK;
+                     n->subLinkId = 0;
                      n->testexpr = NULL;
                      n->operName = NIL;
                      n->subselect = $1;
*************** c_expr:        columnref                                { $$ = $1; }
*** 11307,11312 ****
--- 11350,11356 ----
                      SubLink *n = makeNode(SubLink);
                      A_Indirection *a = makeNode(A_Indirection);
                      n->subLinkType = EXPR_SUBLINK;
+                     n->subLinkId = 0;
                      n->testexpr = NULL;
                      n->operName = NIL;
                      n->subselect = $1;
*************** c_expr:        columnref                                { $$ = $1; }
*** 11319,11324 ****
--- 11363,11369 ----
                  {
                      SubLink *n = makeNode(SubLink);
                      n->subLinkType = EXISTS_SUBLINK;
+                     n->subLinkId = 0;
                      n->testexpr = NULL;
                      n->operName = NIL;
                      n->subselect = $2;
*************** c_expr:        columnref                                { $$ = $1; }
*** 11329,11334 ****
--- 11374,11380 ----
                  {
                      SubLink *n = makeNode(SubLink);
                      n->subLinkType = ARRAY_SUBLINK;
+                     n->subLinkId = 0;
                      n->testexpr = NULL;
                      n->operName = NIL;
                      n->subselect = $2;
diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c
index 0882245..cf87830 100644
*** a/src/backend/parser/parse_expr.c
--- b/src/backend/parser/parse_expr.c
*************** static Node *transformAExprNullIf(ParseS
*** 51,56 ****
--- 51,57 ----
  static Node *transformAExprOf(ParseState *pstate, A_Expr *a);
  static Node *transformAExprIn(ParseState *pstate, A_Expr *a);
  static Node *transformFuncCall(ParseState *pstate, FuncCall *fn);
+ static Node *transformMultiAssignRef(ParseState *pstate, MultiAssignRef *maref);
  static Node *transformCaseExpr(ParseState *pstate, CaseExpr *c);
  static Node *transformSubLink(ParseState *pstate, SubLink *sublink);
  static Node *transformArrayExpr(ParseState *pstate, A_ArrayExpr *a,
*************** static Node *make_row_distinct_op(ParseS
*** 75,80 ****
--- 76,82 ----
                       RowExpr *lrow, RowExpr *rrow, int location);
  static Expr *make_distinct_op(ParseState *pstate, List *opname,
                   Node *ltree, Node *rtree, int location);
+ static int    count_output_columns(List *targetlist);


  /*
*************** transformExprRecurse(ParseState *pstate,
*** 262,267 ****
--- 264,273 ----
              result = transformFuncCall(pstate, (FuncCall *) expr);
              break;

+         case T_MultiAssignRef:
+             result = transformMultiAssignRef(pstate, (MultiAssignRef *) expr);
+             break;
+
          case T_NamedArgExpr:
              {
                  NamedArgExpr *na = (NamedArgExpr *) expr;
*************** transformFuncCall(ParseState *pstate, Fu
*** 1280,1285 ****
--- 1286,1365 ----
  }

  static Node *
+ transformMultiAssignRef(ParseState *pstate, MultiAssignRef *maref)
+ {
+     SubLink    *sublink;
+     Query       *qtree;
+     TargetEntry *tle;
+     Param       *param;
+
+     /* We should only see this in first-stage processing of UPDATE tlists */
+     Assert(pstate->p_expr_kind == EXPR_KIND_UPDATE_SOURCE);
+
+     /* We only need to transform the source if this is the first column */
+     if (maref->colno == 1)
+     {
+         sublink = (SubLink *) transformExprRecurse(pstate, maref->source);
+         /* Currently, the grammar only allows a SubLink as source */
+         Assert(IsA(sublink, SubLink));
+         Assert(sublink->subLinkType == MULTIEXPR_SUBLINK);
+         qtree = (Query *) sublink->subselect;
+         Assert(IsA(qtree, Query));
+
+         /* Check subquery returns required number of columns */
+         if (count_output_columns(qtree->targetList) != maref->ncolumns)
+             ereport(ERROR,
+                     (errcode(ERRCODE_SYNTAX_ERROR),
+                  errmsg("number of columns does not match number of values"),
+                      parser_errposition(pstate, sublink->location)));
+
+         /*
+          * Build a resjunk tlist item containing the MULTIEXPR SubLink, and
+          * add it to pstate->p_multiassign_exprs, whence it will later get
+          * appended to the completed targetlist.  We needn't worry about
+          * selecting a resno for it; transformUpdateStmt will do that.
+          */
+         tle = makeTargetEntry((Expr *) sublink, 0, NULL, true);
+         pstate->p_multiassign_exprs = lappend(pstate->p_multiassign_exprs, tle);
+
+         /*
+          * Assign a unique-within-this-targetlist ID to the MULTIEXPR SubLink.
+          * We can just use its position in the p_multiassign_exprs list.
+          */
+         sublink->subLinkId = list_length(pstate->p_multiassign_exprs);
+     }
+     else
+     {
+         /*
+          * Second or later column in a multiassignment.  Re-fetch the
+          * transformed query, which we assume is still the last entry in
+          * p_multiassign_exprs.
+          */
+         Assert(pstate->p_multiassign_exprs != NIL);
+         tle = (TargetEntry *) llast(pstate->p_multiassign_exprs);
+         sublink = (SubLink *) tle->expr;
+         Assert(IsA(sublink, SubLink));
+         Assert(sublink->subLinkType == MULTIEXPR_SUBLINK);
+         qtree = (Query *) sublink->subselect;
+         Assert(IsA(qtree, Query));
+     }
+
+     /* Build a Param representing the appropriate subquery output column */
+     tle = (TargetEntry *) list_nth(qtree->targetList, maref->colno - 1);
+     Assert(!tle->resjunk);
+
+     param = makeNode(Param);
+     param->paramkind = PARAM_MULTIEXPR;
+     param->paramid = (sublink->subLinkId << 16) | maref->colno;
+     param->paramtype = exprType((Node *) tle->expr);
+     param->paramtypmod = exprTypmod((Node *) tle->expr);
+     param->paramcollid = exprCollation((Node *) tle->expr);
+     param->location = exprLocation((Node *) tle->expr);
+
+     return (Node *) param;
+ }
+
+ static Node *
  transformCaseExpr(ParseState *pstate, CaseExpr *c)
  {
      CaseExpr   *newc;
*************** transformSubLink(ParseState *pstate, Sub
*** 1532,1557 ****
      else if (sublink->subLinkType == EXPR_SUBLINK ||
               sublink->subLinkType == ARRAY_SUBLINK)
      {
-         ListCell   *tlist_item = list_head(qtree->targetList);
-
          /*
           * Make sure the subselect delivers a single column (ignoring resjunk
           * targets).
           */
!         if (tlist_item == NULL ||
!             ((TargetEntry *) lfirst(tlist_item))->resjunk)
              ereport(ERROR,
                      (errcode(ERRCODE_SYNTAX_ERROR),
!                      errmsg("subquery must return a column"),
                       parser_errposition(pstate, sublink->location)));
-         while ((tlist_item = lnext(tlist_item)) != NULL)
-         {
-             if (!((TargetEntry *) lfirst(tlist_item))->resjunk)
-                 ereport(ERROR,
-                         (errcode(ERRCODE_SYNTAX_ERROR),
-                          errmsg("subquery must return only one column"),
-                          parser_errposition(pstate, sublink->location)));
-         }

          /*
           * EXPR and ARRAY need no test expression or combining operator. These
--- 1612,1626 ----
      else if (sublink->subLinkType == EXPR_SUBLINK ||
               sublink->subLinkType == ARRAY_SUBLINK)
      {
          /*
           * Make sure the subselect delivers a single column (ignoring resjunk
           * targets).
           */
!         if (count_output_columns(qtree->targetList) != 1)
              ereport(ERROR,
                      (errcode(ERRCODE_SYNTAX_ERROR),
!                      errmsg("subquery must return only one column"),
                       parser_errposition(pstate, sublink->location)));

          /*
           * EXPR and ARRAY need no test expression or combining operator. These
*************** transformSubLink(ParseState *pstate, Sub
*** 1560,1565 ****
--- 1629,1640 ----
          sublink->testexpr = NULL;
          sublink->operName = NIL;
      }
+     else if (sublink->subLinkType == MULTIEXPR_SUBLINK)
+     {
+         /* Same as EXPR case, except no restriction on number of columns */
+         sublink->testexpr = NULL;
+         sublink->operName = NIL;
+     }
      else
      {
          /* ALL, ANY, or ROWCOMPARE: generate row-comparing expression */
*************** make_distinct_op(ParseState *pstate, Lis
*** 2569,2574 ****
--- 2644,2669 ----
  }

  /*
+  * Count non-resjunk columns in a targetlist
+  */
+ static int
+ count_output_columns(List *targetlist)
+ {
+     int            len = 0;
+     ListCell   *lc;
+
+     foreach(lc, targetlist)
+     {
+         TargetEntry *tle = (TargetEntry *) lfirst(lc);
+
+         Assert(IsA(tle, TargetEntry));
+         if (!tle->resjunk)
+             len++;
+     }
+     return len;
+ }
+
+ /*
   * Produce a string identifying an expression by kind.
   *
   * Note: when practical, use a simple SQL keyword for the result.  If that
diff --git a/src/backend/parser/parse_target.c b/src/backend/parser/parse_target.c
index 2ee1270..328e0c6 100644
*** a/src/backend/parser/parse_target.c
--- b/src/backend/parser/parse_target.c
*************** transformTargetEntry(ParseState *pstate,
*** 113,121 ****
   * transformTargetList()
   * Turns a list of ResTarget's into a list of TargetEntry's.
   *
!  * At this point, we don't care whether we are doing SELECT, UPDATE,
!  * or RETURNING; we just transform the given expressions (the "val" fields).
!  * However, our subroutines care, so we need the exprKind parameter.
   */
  List *
  transformTargetList(ParseState *pstate, List *targetlist,
--- 113,121 ----
   * transformTargetList()
   * Turns a list of ResTarget's into a list of TargetEntry's.
   *
!  * This code acts mostly the same for SELECT, UPDATE, or RETURNING lists;
!  * the main thing is to transform the given expressions (the "val" fields).
!  * The exprKind parameter distinguishes these cases when necesssary.
   */
  List *
  transformTargetList(ParseState *pstate, List *targetlist,
*************** transformTargetList(ParseState *pstate,
*** 124,129 ****
--- 124,132 ----
      List       *p_target = NIL;
      ListCell   *o_target;

+     /* Shouldn't have any leftover multiassign items at start */
+     Assert(pstate->p_multiassign_exprs == NIL);
+
      foreach(o_target, targetlist)
      {
          ResTarget  *res = (ResTarget *) lfirst(o_target);
*************** transformTargetList(ParseState *pstate,
*** 172,177 ****
--- 175,193 ----
                                                  false));
      }

+     /*
+      * If any multiassign resjunk items were created, attach them to the end
+      * of the targetlist.  This should only happen in an UPDATE tlist.  We
+      * don't need to worry about numbering of these items; transformUpdateStmt
+      * will set their resnos.
+      */
+     if (pstate->p_multiassign_exprs)
+     {
+         Assert(exprKind == EXPR_KIND_UPDATE_SOURCE);
+         p_target = list_concat(p_target, pstate->p_multiassign_exprs);
+         pstate->p_multiassign_exprs = NIL;
+     }
+
      return p_target;
  }

*************** transformExpressionList(ParseState *psta
*** 234,239 ****
--- 250,258 ----
                           transformExpr(pstate, e, exprKind));
      }

+     /* Shouldn't have any multiassign items here */
+     Assert(pstate->p_multiassign_exprs == NIL);
+
      return result;
  }

*************** FigureColnameInternal(Node *node, char *
*** 1691,1696 ****
--- 1710,1716 ----
                      }
                      break;
                      /* As with other operator-like nodes, these have no names */
+                 case MULTIEXPR_SUBLINK:
                  case ALL_SUBLINK:
                  case ANY_SUBLINK:
                  case ROWCOMPARE_SUBLINK:
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index 0ab2a13..1f7c6d1 100644
*** a/src/include/nodes/execnodes.h
--- b/src/include/nodes/execnodes.h
*************** typedef struct SubPlanState
*** 728,733 ****
--- 728,734 ----
  {
      ExprState    xprstate;
      struct PlanState *planstate;    /* subselect plan's state tree */
+     struct PlanState *parent;    /* parent plan node's state tree */
      ExprState  *testexpr;        /* state of combining expression */
      List       *args;            /* states of argument expression(s) */
      HeapTuple    curTuple;        /* copy of most recent tuple from subplan */
diff --git a/src/include/nodes/nodes.h b/src/include/nodes/nodes.h
index bc58e16..7b0088f 100644
*** a/src/include/nodes/nodes.h
--- b/src/include/nodes/nodes.h
*************** typedef enum NodeTag
*** 379,384 ****
--- 379,385 ----
      T_A_Indirection,
      T_A_ArrayExpr,
      T_ResTarget,
+     T_MultiAssignRef,
      T_TypeCast,
      T_CollateClause,
      T_SortBy,
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 7e560a1..4758466 100644
*** a/src/include/nodes/parsenodes.h
--- b/src/include/nodes/parsenodes.h
*************** typedef struct ResTarget
*** 388,393 ****
--- 388,410 ----
  } ResTarget;

  /*
+  * MultiAssignRef - element of a row source expression for UPDATE
+  *
+  * In an UPDATE target list, when we have SET (a,b,c) = row-valued-expression,
+  * we generate separate ResTarget items for each of a,b,c.  Their "val" trees
+  * are MultiAssignRef nodes numbered 1..n, linking to a common copy of the
+  * row-valued-expression (which parse analysis will process only once, when
+  * handling the MultiAssignRef with colno=1).
+  */
+ typedef struct MultiAssignRef
+ {
+     NodeTag        type;
+     Node       *source;            /* the row-valued expression */
+     int            colno;            /* column number for this target (1..n) */
+     int            ncolumns;        /* number of targets in the construct */
+ } MultiAssignRef;
+
+ /*
   * SortBy - for ORDER BY clause
   */
  typedef struct SortBy
diff --git a/src/include/nodes/pg_list.h b/src/include/nodes/pg_list.h
index 4167680..c545115 100644
*** a/src/include/nodes/pg_list.h
--- b/src/include/nodes/pg_list.h
*************** extern List *lcons_oid(Oid datum, List *
*** 206,211 ****
--- 206,212 ----
  extern List *list_concat(List *list1, List *list2);
  extern List *list_truncate(List *list, int new_size);

+ extern ListCell *list_nth_cell(const List *list, int n);
  extern void *list_nth(const List *list, int n);
  extern int    list_nth_int(const List *list, int n);
  extern Oid    list_nth_oid(const List *list, int n);
diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h
index 4f03ef9..6949989 100644
*** a/src/include/nodes/primnodes.h
--- b/src/include/nodes/primnodes.h
*************** typedef struct Const
*** 198,214 ****
   *                `paramid' field.  (This type of Param is converted to
   *                PARAM_EXEC during planning.)
   *
!  * Note: currently, paramtypmod is valid for PARAM_SUBLINK Params, and for
!  * PARAM_EXEC Params generated from them; it is always -1 for PARAM_EXTERN
!  * params, since the APIs that supply values for such parameters don't carry
!  * any typmod info.
   * ----------------
   */
  typedef enum ParamKind
  {
      PARAM_EXTERN,
      PARAM_EXEC,
!     PARAM_SUBLINK
  } ParamKind;

  typedef struct Param
--- 198,222 ----
   *                `paramid' field.  (This type of Param is converted to
   *                PARAM_EXEC during planning.)
   *
!  *        PARAM_MULTIEXPR:  Like PARAM_SUBLINK, the parameter represents an
!  *                output column of a SubLink node's sub-select, but here, the
!  *                SubLink is always a MULTIEXPR SubLink.  The high-order 16 bits
!  *                of the `paramid' field contain the SubLink's subLinkId, and
!  *                the low-order 16 bits contain the column number.  (This type
!  *                of Param is also converted to PARAM_EXEC during planning.)
!  *
!  * Note: currently, paramtypmod is always -1 for PARAM_EXTERN params, since
!  * the APIs that supply values for such parameters don't carry any typmod
!  * info.  It is valid in other types of Params, if they represent expressions
!  * with determinable typmod.
   * ----------------
   */
  typedef enum ParamKind
  {
      PARAM_EXTERN,
      PARAM_EXEC,
!     PARAM_SUBLINK,
!     PARAM_MULTIEXPR
  } ParamKind;

  typedef struct Param
*************** typedef struct BoolExpr
*** 489,502 ****
   *    ANY_SUBLINK            (lefthand) op ANY (SELECT ...)
   *    ROWCOMPARE_SUBLINK    (lefthand) op (SELECT ...)
   *    EXPR_SUBLINK        (SELECT with single targetlist item ...)
   *    ARRAY_SUBLINK        ARRAY(SELECT with single targetlist item ...)
   *    CTE_SUBLINK            WITH query (never actually part of an expression)
   * For ALL, ANY, and ROWCOMPARE, the lefthand is a list of expressions of the
   * same length as the subselect's targetlist.  ROWCOMPARE will *always* have
   * a list with more than one entry; if the subselect has just one target
   * then the parser will create an EXPR_SUBLINK instead (and any operator
!  * above the subselect will be represented separately).  Note that both
!  * ROWCOMPARE and EXPR require the subselect to deliver only one row.
   * ALL, ANY, and ROWCOMPARE require the combining operators to deliver boolean
   * results.  ALL and ANY combine the per-row results using AND and OR
   * semantics respectively.
--- 497,512 ----
   *    ANY_SUBLINK            (lefthand) op ANY (SELECT ...)
   *    ROWCOMPARE_SUBLINK    (lefthand) op (SELECT ...)
   *    EXPR_SUBLINK        (SELECT with single targetlist item ...)
+  *    MULTIEXPR_SUBLINK    (SELECT with multiple targetlist items ...)
   *    ARRAY_SUBLINK        ARRAY(SELECT with single targetlist item ...)
   *    CTE_SUBLINK            WITH query (never actually part of an expression)
   * For ALL, ANY, and ROWCOMPARE, the lefthand is a list of expressions of the
   * same length as the subselect's targetlist.  ROWCOMPARE will *always* have
   * a list with more than one entry; if the subselect has just one target
   * then the parser will create an EXPR_SUBLINK instead (and any operator
!  * above the subselect will be represented separately).
!  * ROWCOMPARE, EXPR, and MULTIEXPR require the subselect to deliver at most
!  * one row (if it returns no rows, the result is NULL).
   * ALL, ANY, and ROWCOMPARE require the combining operators to deliver boolean
   * results.  ALL and ANY combine the per-row results using AND and OR
   * semantics respectively.
*************** typedef struct BoolExpr
*** 515,522 ****
   * output columns of the subselect.  And subselect is transformed to a Query.
   * This is the representation seen in saved rules and in the rewriter.
   *
!  * In EXISTS, EXPR, and ARRAY SubLinks, testexpr and operName are unused and
!  * are always null.
   *
   * The CTE_SUBLINK case never occurs in actual SubLink nodes, but it is used
   * in SubPlans generated for WITH subqueries.
--- 525,538 ----
   * output columns of the subselect.  And subselect is transformed to a Query.
   * This is the representation seen in saved rules and in the rewriter.
   *
!  * In EXISTS, EXPR, MULTIEXPR, and ARRAY SubLinks, testexpr and operName
!  * are unused and are always null.
!  *
!  * subLinkId is currently used only for MULTIEXPR SubLinks, and is zero in
!  * other SubLinks.  This number identifies different multiple-assignment
!  * subqueries within an UPDATE statement's SET list.  It is unique only
!  * within a particular targetlist.  The output column(s) of the MULTIEXPR
!  * are referenced by PARAM_MULTIEXPR Params appearing elsewhere in the tlist.
   *
   * The CTE_SUBLINK case never occurs in actual SubLink nodes, but it is used
   * in SubPlans generated for WITH subqueries.
*************** typedef enum SubLinkType
*** 528,533 ****
--- 544,550 ----
      ANY_SUBLINK,
      ROWCOMPARE_SUBLINK,
      EXPR_SUBLINK,
+     MULTIEXPR_SUBLINK,
      ARRAY_SUBLINK,
      CTE_SUBLINK                    /* for SubPlans only */
  } SubLinkType;
*************** typedef struct SubLink
*** 537,545 ****
  {
      Expr        xpr;
      SubLinkType subLinkType;    /* see above */
      Node       *testexpr;        /* outer-query test for ALL/ANY/ROWCOMPARE */
      List       *operName;        /* originally specified operator name */
!     Node       *subselect;        /* subselect as Query* or parsetree */
      int            location;        /* token location, or -1 if unknown */
  } SubLink;

--- 554,563 ----
  {
      Expr        xpr;
      SubLinkType subLinkType;    /* see above */
+     int            subLinkId;        /* ID (1..n); 0 if not MULTIEXPR */
      Node       *testexpr;        /* outer-query test for ALL/ANY/ROWCOMPARE */
      List       *operName;        /* originally specified operator name */
!     Node       *subselect;        /* subselect as Query* or raw parsetree */
      int            location;        /* token location, or -1 if unknown */
  } SubLink;

diff --git a/src/include/nodes/relation.h b/src/include/nodes/relation.h
index 300136e..dacbe9c 100644
*** a/src/include/nodes/relation.h
--- b/src/include/nodes/relation.h
*************** typedef struct PlannerInfo
*** 190,195 ****
--- 190,198 ----

      List       *cte_plan_ids;    /* per-CTE-item list of subplan IDs */

+     List       *multiexpr_params;        /* List of Lists of Params for
+                                          * MULTIEXPR subquery outputs */
+
      List       *eq_classes;        /* list of active EquivalenceClasses */

      List       *canon_pathkeys; /* list of "canonical" PathKeys */
diff --git a/src/include/parser/parse_node.h b/src/include/parser/parse_node.h
index 4ce802a..b32ddf7 100644
*** a/src/include/parser/parse_node.h
--- b/src/include/parser/parse_node.h
*************** struct ParseState
*** 144,149 ****
--- 144,150 ----
      List       *p_windowdefs;    /* raw representations of window clauses */
      ParseExprKind p_expr_kind;    /* what kind of expression we're parsing */
      int            p_next_resno;    /* next targetlist resno to assign */
+     List       *p_multiassign_exprs;    /* junk tlist entries for multiassign */
      List       *p_locking_clause;        /* raw FOR UPDATE/FOR SHARE info */
      Node       *p_value_substitute;        /* what to replace VALUE with, if any */
      bool        p_hasAggs;
diff --git a/src/test/regress/expected/update.out b/src/test/regress/expected/update.out
index 71b856f..1de2a86 100644
*** a/src/test/regress/expected/update.out
--- b/src/test/regress/expected/update.out
*************** SELECT * FROM update_test;
*** 55,85 ****
  --
  -- Test multiple-set-clause syntax
  --
  UPDATE update_test SET (c,b,a) = ('bugle', b+11, DEFAULT) WHERE c = 'foo';
  SELECT * FROM update_test;
    a  | b  |   c
  -----+----+-------
   100 | 20 |
    10 | 31 | bugle
! (2 rows)

  UPDATE update_test SET (c,b) = ('car', a+b), a = a + 1 WHERE a = 10;
  SELECT * FROM update_test;
    a  | b  |  c
  -----+----+-----
   100 | 20 |
    11 | 41 | car
! (2 rows)

  -- fail, multi assignment to same column:
  UPDATE update_test SET (c,b) = ('car', a+b), b = a + 1 WHERE a = 10;
  ERROR:  multiple assignments to same column "b"
! -- XXX this should work, but doesn't yet:
! UPDATE update_test SET (a,b) = (select a,b FROM update_test where c = 'foo')
!   WHERE a = 10;
! ERROR:  syntax error at or near "select"
! LINE 1: UPDATE update_test SET (a,b) = (select a,b FROM update_test ...
!                                         ^
  -- if an alias for the target table is specified, don't allow references
  -- to the original table name
  UPDATE update_test AS t SET b = update_test.b + 10 WHERE t.a = 10;
--- 55,134 ----
  --
  -- Test multiple-set-clause syntax
  --
+ INSERT INTO update_test SELECT a,b+1,c FROM update_test;
+ SELECT * FROM update_test;
+   a  | b  |  c
+ -----+----+-----
+  100 | 20 | foo
+  100 | 20 |
+  100 | 21 | foo
+  100 | 21 |
+ (4 rows)
+
  UPDATE update_test SET (c,b,a) = ('bugle', b+11, DEFAULT) WHERE c = 'foo';
  SELECT * FROM update_test;
    a  | b  |   c
  -----+----+-------
   100 | 20 |
+  100 | 21 |
    10 | 31 | bugle
!   10 | 32 | bugle
! (4 rows)

  UPDATE update_test SET (c,b) = ('car', a+b), a = a + 1 WHERE a = 10;
  SELECT * FROM update_test;
    a  | b  |  c
  -----+----+-----
   100 | 20 |
+  100 | 21 |
    11 | 41 | car
!   11 | 42 | car
! (4 rows)

  -- fail, multi assignment to same column:
  UPDATE update_test SET (c,b) = ('car', a+b), b = a + 1 WHERE a = 10;
  ERROR:  multiple assignments to same column "b"
! -- uncorrelated sub-select:
! UPDATE update_test
!   SET (b,a) = (select a,b from update_test where b = 41 and c = 'car')
!   WHERE a = 100 AND b = 20;
! SELECT * FROM update_test;
!   a  | b  |  c
! -----+----+-----
!  100 | 21 |
!   11 | 41 | car
!   11 | 42 | car
!   41 | 11 |
! (4 rows)
!
! -- correlated sub-select:
! UPDATE update_test o
!   SET (b,a) = (select a+1,b from update_test i
!                where i.a=o.a and i.b=o.b and i.c is not distinct from o.c);
! SELECT * FROM update_test;
!  a  |  b  |  c
! ----+-----+-----
!  21 | 101 |
!  41 |  12 | car
!  42 |  12 | car
!  11 |  42 |
! (4 rows)
!
! -- fail, multiple rows supplied:
! UPDATE update_test SET (b,a) = (select a+1,b from update_test);
! ERROR:  more than one row returned by a subquery used as an expression
! -- set to null if no rows supplied:
! UPDATE update_test SET (b,a) = (select a+1,b from update_test where a = 1000)
!   WHERE a = 11;
! SELECT * FROM update_test;
!  a  |  b  |  c
! ----+-----+-----
!  21 | 101 |
!  41 |  12 | car
!  42 |  12 | car
!     |     |
! (4 rows)
!
  -- if an alias for the target table is specified, don't allow references
  -- to the original table name
  UPDATE update_test AS t SET b = update_test.b + 10 WHERE t.a = 10;
*************** HINT:  Perhaps you meant to reference th
*** 90,99 ****
  -- Make sure that we can update to a TOASTed value.
  UPDATE update_test SET c = repeat('x', 10000) WHERE c = 'car';
  SELECT a, b, char_length(c) FROM update_test;
!   a  | b  | char_length
! -----+----+-------------
!  100 | 20 |
!   11 | 41 |       10000
! (2 rows)

  DROP TABLE update_test;
--- 139,150 ----
  -- Make sure that we can update to a TOASTed value.
  UPDATE update_test SET c = repeat('x', 10000) WHERE c = 'car';
  SELECT a, b, char_length(c) FROM update_test;
!  a  |  b  | char_length
! ----+-----+-------------
!  21 | 101 |
!     |     |
!  41 |  12 |       10000
!  42 |  12 |       10000
! (4 rows)

  DROP TABLE update_test;
diff --git a/src/test/regress/sql/update.sql b/src/test/regress/sql/update.sql
index a8a028f..e71128c 100644
*** a/src/test/regress/sql/update.sql
--- b/src/test/regress/sql/update.sql
*************** SELECT * FROM update_test;
*** 39,44 ****
--- 39,47 ----
  -- Test multiple-set-clause syntax
  --

+ INSERT INTO update_test SELECT a,b+1,c FROM update_test;
+ SELECT * FROM update_test;
+
  UPDATE update_test SET (c,b,a) = ('bugle', b+11, DEFAULT) WHERE c = 'foo';
  SELECT * FROM update_test;
  UPDATE update_test SET (c,b) = ('car', a+b), a = a + 1 WHERE a = 10;
*************** SELECT * FROM update_test;
*** 46,54 ****
  -- fail, multi assignment to same column:
  UPDATE update_test SET (c,b) = ('car', a+b), b = a + 1 WHERE a = 10;

! -- XXX this should work, but doesn't yet:
! UPDATE update_test SET (a,b) = (select a,b FROM update_test where c = 'foo')
!   WHERE a = 10;

  -- if an alias for the target table is specified, don't allow references
  -- to the original table name
--- 49,70 ----
  -- fail, multi assignment to same column:
  UPDATE update_test SET (c,b) = ('car', a+b), b = a + 1 WHERE a = 10;

! -- uncorrelated sub-select:
! UPDATE update_test
!   SET (b,a) = (select a,b from update_test where b = 41 and c = 'car')
!   WHERE a = 100 AND b = 20;
! SELECT * FROM update_test;
! -- correlated sub-select:
! UPDATE update_test o
!   SET (b,a) = (select a+1,b from update_test i
!                where i.a=o.a and i.b=o.b and i.c is not distinct from o.c);
! SELECT * FROM update_test;
! -- fail, multiple rows supplied:
! UPDATE update_test SET (b,a) = (select a+1,b from update_test);
! -- set to null if no rows supplied:
! UPDATE update_test SET (b,a) = (select a+1,b from update_test where a = 1000)
!   WHERE a = 11;
! SELECT * FROM update_test;

  -- if an alias for the target table is specified, don't allow references
  -- to the original table name

pgsql-hackers by date:

Previous
From: Abhijit Menon-Sen
Date:
Subject: [REVIEW] Re: postgresql.auto.conf read from wrong directory
Next
From: Stephen Frost
Date:
Subject: Re: API change advice: Passing plan invalidation info from the rewriter into the planner?