#include "postgres.h"
 
 #include "access/htup_details.h"
+#include "access/tuptoaster.h"
 #include "catalog/heap.h"
 #include "catalog/pg_type.h"
 #include "utils/builtins.h"
 
 /* Other local functions */
 static void ER_mc_callback(void *arg);
-static MemoryContext get_domain_check_cxt(ExpandedRecordHeader *erh);
+static MemoryContext get_short_term_cxt(ExpandedRecordHeader *erh);
 static void build_dummy_expanded_header(ExpandedRecordHeader *main_erh);
 static pg_noinline void check_domain_for_new_field(ExpandedRecordHeader *erh,
                           int fnumber,
  *
  * The expanded record is initially "empty", having a state logically
  * equivalent to a NULL composite value (not ROW(NULL, NULL, ...)).
- * Note that this might not be a valid state for a domain type; if the
- * caller needs to check that, call expanded_record_set_tuple(erh, NULL).
+ * Note that this might not be a valid state for a domain type;
+ * if the caller needs to check that, call
+ * expanded_record_set_tuple(erh, NULL, false, false).
  *
  * The expanded object will be a child of parentcontext.
  */
  *
  * The tuple is physically copied into the expanded record's local storage
  * if "copy" is true, otherwise it's caller's responsibility that the tuple
- * will live as long as the expanded record does.  In any case, out-of-line
- * fields in the tuple are not automatically inlined.
+ * will live as long as the expanded record does.
+ *
+ * Out-of-line field values in the tuple are automatically inlined if
+ * "expand_external" is true, otherwise not.  (The combination copy = false,
+ * expand_external = true is not sensible and not supported.)
  *
  * Alternatively, tuple can be NULL, in which case we just set the expanded
  * record to be empty.
 void
 expanded_record_set_tuple(ExpandedRecordHeader *erh,
                          HeapTuple tuple,
-                         bool copy)
+                         bool copy,
+                         bool expand_external)
 {
    int         oldflags;
    HeapTuple   oldtuple;
    if (erh->flags & ER_FLAG_IS_DOMAIN)
        check_domain_for_new_tuple(erh, tuple);
 
+   /*
+    * If we need to get rid of out-of-line field values, do so, using the
+    * short-term context to avoid leaking whatever cruft the toast fetch
+    * might generate.
+    */
+   if (expand_external && tuple)
+   {
+       /* Assert caller didn't ask for unsupported case */
+       Assert(copy);
+       if (HeapTupleHasExternal(tuple))
+       {
+           oldcxt = MemoryContextSwitchTo(get_short_term_cxt(erh));
+           tuple = toast_flatten_tuple(tuple, erh->er_tupdesc);
+           MemoryContextSwitchTo(oldcxt);
+       }
+       else
+           expand_external = false;    /* need not clean up below */
+   }
+
    /*
     * Initialize new flags, keeping only non-data status bits.
     */
        newtuple = heap_copytuple(tuple);
        newflags |= ER_FLAG_FVALUE_ALLOCED;
        MemoryContextSwitchTo(oldcxt);
+
+       /* We can now flush anything that detoasting might have leaked. */
+       if (expand_external)
+           MemoryContextReset(erh->er_short_term_cxt);
    }
    else
        newtuple = tuple;
                VARATT_IS_EXTERNAL(DatumGetPointer(erh->dvalues[i])))
            {
                /*
-                * It's an external toasted value, so we need to dereference
-                * it so that the flat representation will be self-contained.
-                * Do this step in the caller's context because the TOAST
-                * fetch might leak memory.  That means making an extra copy,
-                * which is a tad annoying, but repetitive leaks in the
-                * record's context would be worse.
+                * expanded_record_set_field_internal can do the actual work
+                * of detoasting.  It needn't recheck domain constraints.
                 */
-               Datum       newValue;
-
-               newValue = PointerGetDatum(PG_DETOAST_DATUM(erh->dvalues[i]));
-               /* expanded_record_set_field can do the rest */
-               /* ... and we don't need it to recheck domain constraints */
                expanded_record_set_field_internal(erh, i + 1,
-                                                  newValue, false,
+                                                  erh->dvalues[i], false,
+                                                  true,
                                                   false);
-               /* Might as well free the detoasted value */
-               pfree(DatumGetPointer(newValue));
            }
        }
 
  * (without changing the record's state) if the domain's constraints would
  * be violated.
  *
+ * If expand_external is true and newValue is an out-of-line value, we'll
+ * forcibly detoast it so that the record does not depend on external storage.
+ *
  * Internal callers can pass check_constraints = false to skip application
  * of domain constraints.  External callers should never do that.
  */
 void
 expanded_record_set_field_internal(ExpandedRecordHeader *erh, int fnumber,
                                   Datum newValue, bool isnull,
+                                  bool expand_external,
                                   bool check_constraints)
 {
    TupleDesc   tupdesc;
        elog(ERROR, "cannot assign to field %d of expanded record", fnumber);
 
    /*
-    * Copy new field value into record's context, if needed.
+    * Copy new field value into record's context, and deal with detoasting,
+    * if needed.
     */
    attr = TupleDescAttr(tupdesc, fnumber - 1);
    if (!isnull && !attr->attbyval)
    {
        MemoryContext oldcxt;
 
+       /* If requested, detoast any external value */
+       if (expand_external)
+       {
+           if (attr->attlen == -1 &&
+               VARATT_IS_EXTERNAL(DatumGetPointer(newValue)))
+           {
+               /* Detoasting should be done in short-lived context. */
+               oldcxt = MemoryContextSwitchTo(get_short_term_cxt(erh));
+               newValue = PointerGetDatum(heap_tuple_fetch_attr((struct varlena *) DatumGetPointer(newValue)));
+               MemoryContextSwitchTo(oldcxt);
+           }
+           else
+               expand_external = false;    /* need not clean up below */
+       }
+
+       /* Copy value into record's context */
        oldcxt = MemoryContextSwitchTo(erh->hdr.eoh_context);
        newValue = datumCopy(newValue, false, attr->attlen);
        MemoryContextSwitchTo(oldcxt);
 
+       /* We can now flush anything that detoasting might have leaked */
+       if (expand_external)
+           MemoryContextReset(erh->er_short_term_cxt);
+
        /* Remember that we have field(s) that may need to be pfree'd */
        erh->flags |= ER_FLAG_DVALUES_ALLOCED;
 
        /*
         * While we're here, note whether it's an external toasted value,
-        * because that could mean we need to inline it later.
+        * because that could mean we need to inline it later.  (Think not to
+        * merge this into the previous expand_external logic: datumCopy could
+        * by itself have made the value non-external.)
         */
        if (attr->attlen == -1 &&
            VARATT_IS_EXTERNAL(DatumGetPointer(newValue)))
  * Caller must ensure that the provided datums are of the right types
  * to match the record's previously assigned rowtype.
  *
+ * If expand_external is true, we'll forcibly detoast out-of-line field values
+ * so that the record does not depend on external storage.
+ *
  * Unlike repeated application of expanded_record_set_field(), this does not
  * guarantee to leave the expanded record in a non-corrupt state in event
  * of an error.  Typically it would only be used for initializing a new
- * expanded record.
+ * expanded record.  Also, because we expect this to be applied at most once
+ * in the lifespan of an expanded record, we do not worry about any cruft
+ * that detoasting might leak.
  */
 void
 expanded_record_set_fields(ExpandedRecordHeader *erh,
-                          const Datum *newValues, const bool *isnulls)
+                          const Datum *newValues, const bool *isnulls,
+                          bool expand_external)
 {
    TupleDesc   tupdesc;
    Datum      *dvalues;
        if (!attr->attbyval)
        {
            /*
-            * Copy new field value into record's context, if needed.
+            * Copy new field value into record's context, and deal with
+            * detoasting, if needed.
             */
            if (!isnull)
            {
-               newValue = datumCopy(newValue, false, attr->attlen);
+               /* Is it an external toasted value? */
+               if (attr->attlen == -1 &&
+                   VARATT_IS_EXTERNAL(DatumGetPointer(newValue)))
+               {
+                   if (expand_external)
+                   {
+                       /* Detoast as requested while copying the value */
+                       newValue = PointerGetDatum(heap_tuple_fetch_attr((struct varlena *) DatumGetPointer(newValue)));
+                   }
+                   else
+                   {
+                       /* Just copy the value */
+                       newValue = datumCopy(newValue, false, -1);
+                       /* If it's still external, remember that */
+                       if (VARATT_IS_EXTERNAL(DatumGetPointer(newValue)))
+                           erh->flags |= ER_FLAG_HAVE_EXTERNAL;
+                   }
+               }
+               else
+               {
+                   /* Not an external value, just copy it */
+                   newValue = datumCopy(newValue, false, attr->attlen);
+               }
 
                /* Remember that we have field(s) that need to be pfree'd */
                erh->flags |= ER_FLAG_DVALUES_ALLOCED;
-
-               /*
-                * While we're here, note whether it's an external toasted
-                * value, because that could mean we need to inline it later.
-                */
-               if (attr->attlen == -1 &&
-                   VARATT_IS_EXTERNAL(DatumGetPointer(newValue)))
-                   erh->flags |= ER_FLAG_HAVE_EXTERNAL;
            }
 
            /*
    if (erh->flags & ER_FLAG_IS_DOMAIN)
    {
        /* We run domain_check in a short-lived context to limit cruft */
-       MemoryContextSwitchTo(get_domain_check_cxt(erh));
+       MemoryContextSwitchTo(get_short_term_cxt(erh));
 
        domain_check(ExpandedRecordGetRODatum(erh), false,
                     erh->er_decltypeid,
 }
 
 /*
- * Construct (or reset) working memory context for domain checks.
+ * Construct (or reset) working memory context for short-term operations.
+ *
+ * This context is used for domain check evaluation and for detoasting.
  *
- * If we don't have a working memory context for domain checking, make one;
- * if we have one, reset it to get rid of any leftover cruft.  (It is a tad
- * annoying to need a whole context for this, since it will often go unused
- * --- but it's hard to avoid memory leaks otherwise.  We can make the
- * context small, at least.)
+ * If we don't have a short-lived memory context, make one; if we have one,
+ * reset it to get rid of any leftover cruft.  (It is a tad annoying to need a
+ * whole context for this, since it will often go unused --- but it's hard to
+ * avoid memory leaks otherwise.  We can make the context small, at least.)
  */
 static MemoryContext
-get_domain_check_cxt(ExpandedRecordHeader *erh)
+get_short_term_cxt(ExpandedRecordHeader *erh)
 {
-   if (erh->er_domain_check_cxt == NULL)
-       erh->er_domain_check_cxt =
+   if (erh->er_short_term_cxt == NULL)
+       erh->er_short_term_cxt =
            AllocSetContextCreate(erh->hdr.eoh_context,
-                                 "expanded record domain checks",
+                                 "expanded record short-term context",
                                  ALLOCSET_SMALL_SIZES);
    else
-       MemoryContextReset(erh->er_domain_check_cxt);
-   return erh->er_domain_check_cxt;
+       MemoryContextReset(erh->er_short_term_cxt);
+   return erh->er_short_term_cxt;
 }
 
 /*
    ExpandedRecordHeader *erh;
    TupleDesc   tupdesc = expanded_record_get_tupdesc(main_erh);
 
-   /* Ensure we have a domain_check_cxt */
-   (void) get_domain_check_cxt(main_erh);
+   /* Ensure we have a short-lived context */
+   (void) get_short_term_cxt(main_erh);
 
    /*
     * Allocate dummy header on first time through, or in the unlikely event
         * nothing else is authorized to delete or transfer ownership of the
         * object's context, so it should be safe enough.
         */
-       EOH_init_header(&erh->hdr, &ER_methods, main_erh->er_domain_check_cxt);
+       EOH_init_header(&erh->hdr, &ER_methods, main_erh->er_short_term_cxt);
        erh->er_magic = ER_MAGIC;
 
        /* Set up dvalues/dnulls, with no valid contents as yet */
     * We call domain_check in the short-lived context, so that any cruft
     * leaked by expression evaluation can be reclaimed.
     */
-   oldcxt = MemoryContextSwitchTo(erh->er_domain_check_cxt);
+   oldcxt = MemoryContextSwitchTo(erh->er_short_term_cxt);
 
    /*
     * And now we can apply the check.  Note we use main header's domain cache
    MemoryContextSwitchTo(oldcxt);
 
    /* We might as well clean up cruft immediately. */
-   MemoryContextReset(erh->er_domain_check_cxt);
+   MemoryContextReset(erh->er_short_term_cxt);
 }
 
 /*
    if (tuple == NULL)
    {
        /* We run domain_check in a short-lived context to limit cruft */
-       oldcxt = MemoryContextSwitchTo(get_domain_check_cxt(erh));
+       oldcxt = MemoryContextSwitchTo(get_short_term_cxt(erh));
 
        domain_check((Datum) 0, true,
                     erh->er_decltypeid,
        MemoryContextSwitchTo(oldcxt);
 
        /* We might as well clean up cruft immediately. */
-       MemoryContextReset(erh->er_domain_check_cxt);
+       MemoryContextReset(erh->er_short_term_cxt);
 
        return;
    }
     * We call domain_check in the short-lived context, so that any cruft
     * leaked by expression evaluation can be reclaimed.
     */
-   oldcxt = MemoryContextSwitchTo(erh->er_domain_check_cxt);
+   oldcxt = MemoryContextSwitchTo(erh->er_short_term_cxt);
 
    /*
     * And now we can apply the check.  Note we use main header's domain cache
    MemoryContextSwitchTo(oldcxt);
 
    /* We might as well clean up cruft immediately. */
-   MemoryContextReset(erh->er_domain_check_cxt);
+   MemoryContextReset(erh->er_short_term_cxt);
 }
 
    (VARATT_IS_EXTERNAL(PTR) && VARTAG_EXTERNAL(PTR) == VARTAG_EXPANDED_RW)
 #define VARATT_IS_EXTERNAL_EXPANDED(PTR) \
    (VARATT_IS_EXTERNAL(PTR) && VARTAG_IS_EXPANDED(VARTAG_EXTERNAL(PTR)))
+#define VARATT_IS_EXTERNAL_NON_EXPANDED(PTR) \
+   (VARATT_IS_EXTERNAL(PTR) && !VARTAG_IS_EXPANDED(VARTAG_EXTERNAL(PTR)))
 #define VARATT_IS_SHORT(PTR)               VARATT_IS_1B(PTR)
 #define VARATT_IS_EXTENDED(PTR)                (!VARATT_IS_4B_U(PTR))
 
 
    char       *fstartptr;      /* start of its data area */
    char       *fendptr;        /* end+1 of its data area */
 
+   /* Some operations on the expanded record need a short-lived context */
+   MemoryContext er_short_term_cxt;    /* short-term memory context */
+
    /* Working state for domain checking, used if ER_FLAG_IS_DOMAIN is set */
-   MemoryContext er_domain_check_cxt;  /* short-term memory context */
    struct ExpandedRecordHeader *er_dummy_header;   /* dummy record header */
    void       *er_domaininfo;  /* cache space for domain_check() */
 
 extern ExpandedRecordHeader *make_expanded_record_from_exprecord(ExpandedRecordHeader *olderh,
                                    MemoryContext parentcontext);
 extern void expanded_record_set_tuple(ExpandedRecordHeader *erh,
-                         HeapTuple tuple, bool copy);
+                         HeapTuple tuple, bool copy, bool expand_external);
 extern Datum make_expanded_record_from_datum(Datum recorddatum,
                                MemoryContext parentcontext);
 extern TupleDesc expanded_record_fetch_tupdesc(ExpandedRecordHeader *erh);
 extern void expanded_record_set_field_internal(ExpandedRecordHeader *erh,
                                   int fnumber,
                                   Datum newValue, bool isnull,
+                                  bool expand_external,
                                   bool check_constraints);
 extern void expanded_record_set_fields(ExpandedRecordHeader *erh,
-                          const Datum *newValues, const bool *isnulls);
+                          const Datum *newValues, const bool *isnulls,
+                          bool expand_external);
 
 /* outside code should never call expanded_record_set_field_internal as such */
-#define expanded_record_set_field(erh, fnumber, newValue, isnull) \
-   expanded_record_set_field_internal(erh, fnumber, newValue, isnull, true)
+#define expanded_record_set_field(erh, fnumber, newValue, isnull, expand_external) \
+   expanded_record_set_field_internal(erh, fnumber, newValue, isnull, expand_external, true)
 
 /*
  * Inline-able fast cases.  The expanded_record_fetch_xxx functions above
 
 #include "access/htup_details.h"
 #include "access/transam.h"
 #include "access/tupconvert.h"
+#include "access/tuptoaster.h"
 #include "catalog/pg_proc.h"
 #include "catalog/pg_type.h"
 #include "commands/defrem.h"
    }
    else if (TRIGGER_FIRED_BY_INSERT(trigdata->tg_event))
    {
-       expanded_record_set_tuple(rec_new->erh, trigdata->tg_trigtuple, false);
+       expanded_record_set_tuple(rec_new->erh, trigdata->tg_trigtuple,
+                                 false, false);
    }
    else if (TRIGGER_FIRED_BY_UPDATE(trigdata->tg_event))
    {
-       expanded_record_set_tuple(rec_new->erh, trigdata->tg_newtuple, false);
-       expanded_record_set_tuple(rec_old->erh, trigdata->tg_trigtuple, false);
+       expanded_record_set_tuple(rec_new->erh, trigdata->tg_newtuple,
+                                 false, false);
+       expanded_record_set_tuple(rec_old->erh, trigdata->tg_trigtuple,
+                                 false, false);
    }
    else if (TRIGGER_FIRED_BY_DELETE(trigdata->tg_event))
    {
-       expanded_record_set_tuple(rec_old->erh, trigdata->tg_trigtuple, false);
+       expanded_record_set_tuple(rec_old->erh, trigdata->tg_trigtuple,
+                                 false, false);
    }
    else
        elog(ERROR, "unrecognized trigger action: not INSERT, DELETE, or UPDATE");
 
                /* And assign it. */
                expanded_record_set_field(erh, recfield->finfo.fnumber,
-                                         value, isNull);
+                                         value, isNull, !estate->atomic);
                break;
            }
 
                    tupdescs_match)
                {
                    /* Only need to assign a new tuple value */
-                   expanded_record_set_tuple(rec->erh, tuptab->vals[i], true);
+                   expanded_record_set_tuple(rec->erh, tuptab->vals[i],
+                                             true, !estate->atomic);
                }
                else
                {
                 */
                newerh = make_expanded_record_for_rec(estate, rec,
                                                      NULL, rec->erh);
-               expanded_record_set_tuple(newerh, NULL, false);
+               expanded_record_set_tuple(newerh, NULL, false, false);
                assign_record_var(estate, rec, newerh);
            }
            else
            else
            {
                /* No coercion is needed, so just assign the row value */
-               expanded_record_set_tuple(newerh, tup, true);
+               expanded_record_set_tuple(newerh, tup, true, !estate->atomic);
            }
 
            /* Complete the assignment */
        }
 
        /* Insert the coerced field values into the new expanded record */
-       expanded_record_set_fields(newerh, values, nulls);
+       expanded_record_set_fields(newerh, values, nulls, !estate->atomic);
 
        /* Complete the assignment */
        assign_record_var(estate, rec, newerh);
                 (erh->er_typmod == rec->erh->er_typmod &&
                  erh->er_typmod >= 0)))
            {
-               expanded_record_set_tuple(rec->erh, erh->fvalue, true);
+               expanded_record_set_tuple(rec->erh, erh->fvalue,
+                                         true, !estate->atomic);
                return;
            }
 
                (rec->rectypeid == RECORDOID ||
                 rec->rectypeid == erh->er_typeid))
            {
-               expanded_record_set_tuple(newerh, erh->fvalue, true);
+               expanded_record_set_tuple(newerh, erh->fvalue,
+                                         true, !estate->atomic);
                assign_record_var(estate, rec, newerh);
                return;
            }
                 (tupTypmod == rec->erh->er_typmod &&
                  tupTypmod >= 0)))
            {
-               expanded_record_set_tuple(rec->erh, &tmptup, true);
+               expanded_record_set_tuple(rec->erh, &tmptup,
+                                         true, !estate->atomic);
                return;
            }
 
 
                newerh = make_expanded_record_from_typeid(tupType, tupTypmod,
                                                          mcontext);
-               expanded_record_set_tuple(newerh, &tmptup, true);
+               expanded_record_set_tuple(newerh, &tmptup,
+                                         true, !estate->atomic);
                assign_record_var(estate, rec, newerh);
                return;
            }
  * assign_simple_var --- assign a new value to any VAR datum.
  *
  * This should be the only mechanism for assignment to simple variables,
- * lest we do the release of the old value incorrectly.
+ * lest we do the release of the old value incorrectly (not to mention
+ * the detoasting business).
  */
 static void
 assign_simple_var(PLpgSQL_execstate *estate, PLpgSQL_var *var,
 {
    Assert(var->dtype == PLPGSQL_DTYPE_VAR ||
           var->dtype == PLPGSQL_DTYPE_PROMISE);
+
+   /*
+    * In non-atomic contexts, we do not want to store TOAST pointers in
+    * variables, because such pointers might become stale after a commit.
+    * Forcibly detoast in such cases.  We don't want to detoast (flatten)
+    * expanded objects, however; those should be OK across a transaction
+    * boundary since they're just memory-resident objects.  (Elsewhere in
+    * this module, operations on expanded records likewise need to request
+    * detoasting of record fields when !estate->atomic.  Expanded arrays are
+    * not a problem since all array entries are always detoasted.)
+    */
+   if (!estate->atomic && !isnull && var->datatype->typlen == -1 &&
+       VARATT_IS_EXTERNAL_NON_EXPANDED(DatumGetPointer(newvalue)))
+   {
+       MemoryContext oldcxt;
+       Datum       detoasted;
+
+       /*
+        * Do the detoasting in the eval_mcontext to avoid long-term leakage
+        * of whatever memory toast fetching might leak.  Then we have to copy
+        * the detoasted datum to the function's main context, which is a
+        * pain, but there's little choice.
+        */
+       oldcxt = MemoryContextSwitchTo(get_eval_mcontext(estate));
+       detoasted = PointerGetDatum(heap_tuple_fetch_attr((struct varlena *) DatumGetPointer(newvalue)));
+       MemoryContextSwitchTo(oldcxt);
+       /* Now's a good time to not leak the input value if it's freeable */
+       if (freeable)
+           pfree(DatumGetPointer(newvalue));
+       /* Once we copy the value, it's definitely freeable */
+       newvalue = datumCopy(detoasted, false, -1);
+       freeable = true;
+       /* Can't clean up eval_mcontext here, but it'll happen before long */
+   }
+
    /* Free the old value if needed */
    if (var->freeval)
    {
 
--- /dev/null
+Parsed test spec with 2 sessions
+
+starting permutation: lock assign1 vacuum unlock
+pg_advisory_unlock_all
+
+               
+pg_advisory_unlock_all
+
+               
+step lock: 
+    SELECT pg_advisory_lock(1);
+
+pg_advisory_lock
+
+               
+step assign1: 
+do $$
+  declare
+    x text;
+  begin
+    select test1.b into x from test1;
+    delete from test1;
+    commit;
+    perform pg_advisory_lock(1);
+    raise notice 'x = %', x;
+  end;
+$$;
+ <waiting ...>
+step vacuum: 
+    VACUUM test1;
+
+step unlock: 
+    SELECT pg_advisory_unlock(1);
+
+pg_advisory_unlock
+
+t              
+step assign1: <... completed>
+
+starting permutation: lock assign2 vacuum unlock
+pg_advisory_unlock_all
+
+               
+pg_advisory_unlock_all
+
+               
+step lock: 
+    SELECT pg_advisory_lock(1);
+
+pg_advisory_lock
+
+               
+step assign2: 
+do $$
+  declare
+    x text;
+  begin
+    x := (select test1.b from test1);
+    delete from test1;
+    commit;
+    perform pg_advisory_lock(1);
+    raise notice 'x = %', x;
+  end;
+$$;
+ <waiting ...>
+step vacuum: 
+    VACUUM test1;
+
+step unlock: 
+    SELECT pg_advisory_unlock(1);
+
+pg_advisory_unlock
+
+t              
+step assign2: <... completed>
+
+starting permutation: lock assign3 vacuum unlock
+pg_advisory_unlock_all
+
+               
+pg_advisory_unlock_all
+
+               
+step lock: 
+    SELECT pg_advisory_lock(1);
+
+pg_advisory_lock
+
+               
+step assign3: 
+do $$
+  declare
+    r record;
+  begin
+    select * into r from test1;
+    r.b := (select test1.b from test1);
+    delete from test1;
+    commit;
+    perform pg_advisory_lock(1);
+    raise notice 'r = %', r;
+  end;
+$$;
+ <waiting ...>
+step vacuum: 
+    VACUUM test1;
+
+step unlock: 
+    SELECT pg_advisory_unlock(1);
+
+pg_advisory_unlock
+
+t              
+step assign3: <... completed>
+
+starting permutation: lock assign4 vacuum unlock
+pg_advisory_unlock_all
+
+               
+pg_advisory_unlock_all
+
+               
+step lock: 
+    SELECT pg_advisory_lock(1);
+
+pg_advisory_lock
+
+               
+step assign4: 
+do $$
+  declare
+    r test2;
+  begin
+    select * into r from test1;
+    delete from test1;
+    commit;
+    perform pg_advisory_lock(1);
+    raise notice 'r = %', r;
+  end;
+$$;
+ <waiting ...>
+step vacuum: 
+    VACUUM test1;
+
+step unlock: 
+    SELECT pg_advisory_unlock(1);
+
+pg_advisory_unlock
+
+t              
+step assign4: <... completed>
+
+starting permutation: lock assign5 vacuum unlock
+pg_advisory_unlock_all
+
+               
+pg_advisory_unlock_all
+
+               
+step lock: 
+    SELECT pg_advisory_lock(1);
+
+pg_advisory_lock
+
+               
+step assign5: 
+do $$
+  declare
+    r record;
+  begin
+    for r in select test1.b from test1 loop
+      null;
+    end loop;
+    delete from test1;
+    commit;
+    perform pg_advisory_lock(1);
+    raise notice 'r = %', r;
+  end;
+$$;
+ <waiting ...>
+step vacuum: 
+    VACUUM test1;
+
+step unlock: 
+    SELECT pg_advisory_unlock(1);
+
+pg_advisory_unlock
+
+t              
+step assign5: <... completed>
 
 test: partition-key-update-1
 test: partition-key-update-2
 test: partition-key-update-3
+test: plpgsql-toast
 
--- /dev/null
+# Test TOAST behavior in PL/pgSQL procedures with transaction control.
+#
+# We need to ensure that values stored in PL/pgSQL variables are free
+# of external TOAST references, because those could disappear after a
+# transaction is committed (leading to errors "missing chunk number
+# ... for toast value ...").  The tests here do this by running VACUUM
+# in a second session.  Advisory locks are used to have the VACUUM
+# kick in at the right time.  The different "assign" steps test
+# different code paths for variable assignments in PL/pgSQL.
+
+setup
+{
+    CREATE TABLE test1 (a int, b text);
+    ALTER TABLE test1 ALTER COLUMN b SET STORAGE EXTERNAL;
+    INSERT INTO test1 VALUES (1, repeat('foo', 2000));
+    CREATE TYPE test2 AS (a bigint, b text);
+}
+
+teardown
+{
+    DROP TABLE test1;
+    DROP TYPE test2;
+}
+
+session "s1"
+
+setup
+{
+    SELECT pg_advisory_unlock_all();
+}
+
+# assign_simple_var()
+step "assign1"
+{
+do $$
+  declare
+    x text;
+  begin
+    select test1.b into x from test1;
+    delete from test1;
+    commit;
+    perform pg_advisory_lock(1);
+    raise notice 'x = %', x;
+  end;
+$$;
+}
+
+# assign_simple_var()
+step "assign2"
+{
+do $$
+  declare
+    x text;
+  begin
+    x := (select test1.b from test1);
+    delete from test1;
+    commit;
+    perform pg_advisory_lock(1);
+    raise notice 'x = %', x;
+  end;
+$$;
+}
+
+# expanded_record_set_field()
+step "assign3"
+{
+do $$
+  declare
+    r record;
+  begin
+    select * into r from test1;
+    r.b := (select test1.b from test1);
+    delete from test1;
+    commit;
+    perform pg_advisory_lock(1);
+    raise notice 'r = %', r;
+  end;
+$$;
+}
+
+# expanded_record_set_fields()
+step "assign4"
+{
+do $$
+  declare
+    r test2;
+  begin
+    select * into r from test1;
+    delete from test1;
+    commit;
+    perform pg_advisory_lock(1);
+    raise notice 'r = %', r;
+  end;
+$$;
+}
+
+# expanded_record_set_tuple()
+step "assign5"
+{
+do $$
+  declare
+    r record;
+  begin
+    for r in select test1.b from test1 loop
+      null;
+    end loop;
+    delete from test1;
+    commit;
+    perform pg_advisory_lock(1);
+    raise notice 'r = %', r;
+  end;
+$$;
+}
+
+session "s2"
+setup
+{
+    SELECT pg_advisory_unlock_all();
+}
+step "lock"
+{
+    SELECT pg_advisory_lock(1);
+}
+step "vacuum"
+{
+    VACUUM test1;
+}
+step "unlock"
+{
+    SELECT pg_advisory_unlock(1);
+}
+
+permutation "lock" "assign1" "vacuum" "unlock"
+permutation "lock" "assign2" "vacuum" "unlock"
+permutation "lock" "assign3" "vacuum" "unlock"
+permutation "lock" "assign4" "vacuum" "unlock"
+permutation "lock" "assign5" "vacuum" "unlock"