memcpy((char *) dest->t_data, (char *) src->t_data, src->t_len);
}
+/* ----------------
+ * heap_copy_tuple_as_datum
+ *
+ * copy a tuple as a composite-type Datum
+ * ----------------
+ */
+Datum
+heap_copy_tuple_as_datum(HeapTuple tuple, TupleDesc tupleDesc)
+{
+ HeapTupleHeader td;
+
+ /*
+ * If the tuple contains any external TOAST pointers, we have to inline
+ * those fields to meet the conventions for composite-type Datums.
+ */
+ if (HeapTupleHasExternal(tuple))
+ return toast_flatten_tuple_to_datum(tuple->t_data,
+ tuple->t_len,
+ tupleDesc);
+
+ /*
+ * Fast path for easy case: just make a palloc'd copy and insert the
+ * correct composite-Datum header fields (since those may not be set if
+ * the given tuple came from disk, rather than from heap_form_tuple).
+ */
+ td = (HeapTupleHeader) palloc(tuple->t_len);
+ memcpy((char *) td, (char *) tuple->t_data, tuple->t_len);
+
+ HeapTupleHeaderSetDatumLength(td, tuple->t_len);
+ HeapTupleHeaderSetTypeId(td, tupleDesc->tdtypeid);
+ HeapTupleHeaderSetTypMod(td, tupleDesc->tdtypmod);
+
+ return PointerGetDatum(td);
+}
+
/*
* heap_form_tuple
* construct a tuple from the given values[] and isnull[] arrays,
data_len;
int hoff;
bool hasnull = false;
- Form_pg_attribute *att = tupleDescriptor->attrs;
int numberOfAttributes = tupleDescriptor->natts;
int i;
numberOfAttributes, MaxTupleAttributeNumber)));
/*
- * Check for nulls and embedded tuples; expand any toasted attributes in
- * embedded tuples. This preserves the invariant that toasting can only
- * go one level deep.
- *
- * We can skip calling toast_flatten_tuple_attribute() if the attribute
- * couldn't possibly be of composite type. All composite datums are
- * varlena and have alignment 'd'; furthermore they aren't arrays. Also,
- * if an attribute is already toasted, it must have been sent to disk
- * already and so cannot contain toasted attributes.
+ * Check for nulls
*/
for (i = 0; i < numberOfAttributes; i++)
{
if (isnull[i])
- hasnull = true;
- else if (att[i]->attlen == -1 &&
- att[i]->attalign == 'd' &&
- att[i]->attndims == 0 &&
- !VARATT_IS_EXTENDED(DatumGetPointer(values[i])))
{
- values[i] = toast_flatten_tuple_attribute(values[i],
- att[i]->atttypid,
- att[i]->atttypmod);
+ hasnull = true;
+ break;
}
}
/*
* And fill in the information. Note we fill the Datum fields even though
- * this tuple may never become a Datum.
+ * this tuple may never become a Datum. This lets HeapTupleHeaderGetDatum
+ * identify the tuple type if needed.
*/
tuple->t_len = len;
ItemPointerSetInvalid(&(tuple->t_self));
data_len;
int hoff;
bool hasnull = false;
- Form_pg_attribute *att = tupleDescriptor->attrs;
int numberOfAttributes = tupleDescriptor->natts;
int i;
numberOfAttributes, MaxTupleAttributeNumber)));
/*
- * Check for nulls and embedded tuples; expand any toasted attributes in
- * embedded tuples. This preserves the invariant that toasting can only
- * go one level deep.
- *
- * We can skip calling toast_flatten_tuple_attribute() if the attribute
- * couldn't possibly be of composite type. All composite datums are
- * varlena and have alignment 'd'; furthermore they aren't arrays. Also,
- * if an attribute is already toasted, it must have been sent to disk
- * already and so cannot contain toasted attributes.
+ * Check for nulls
*/
for (i = 0; i < numberOfAttributes; i++)
{
if (isnull[i])
- hasnull = true;
- else if (att[i]->attlen == -1 &&
- att[i]->attalign == 'd' &&
- att[i]->attndims == 0 &&
- !VARATT_IS_EXTENDED(values[i]))
{
- values[i] = toast_flatten_tuple_attribute(values[i],
- att[i]->atttypid,
- att[i]->atttypmod);
+ hasnull = true;
+ break;
}
}
if (tupmask & HEAP_HASVARWIDTH)
infomask |= INDEX_VAR_MASK;
+ /* Also assert we got rid of external attributes */
+#ifdef TOAST_INDEX_HACK
+ Assert((tupmask & HEAP_HASEXTERNAL) == 0);
+#endif
+
/*
* Here we make sure that the size will fit in the field reserved for it
* in t_info.
*
* "Flatten" a tuple to contain no out-of-line toasted fields.
* (This does not eliminate compressed or short-header datums.)
+ *
+ * Note: we expect the caller already checked HeapTupleHasExternal(tup),
+ * so there is no need for a short-circuit path.
* ----------
*/
HeapTuple
/* ----------
- * toast_flatten_tuple_attribute -
+ * toast_flatten_tuple_to_datum -
+ *
+ * "Flatten" a tuple containing out-of-line toasted fields into a Datum.
+ * The result is always palloc'd in the current memory context.
+ *
+ * We have a general rule that Datums of container types (rows, arrays,
+ * ranges, etc) must not contain any external TOAST pointers. Without
+ * this rule, we'd have to look inside each Datum when preparing a tuple
+ * for storage, which would be expensive and would fail to extend cleanly
+ * to new sorts of container types.
+ *
+ * However, we don't want to say that tuples represented as HeapTuples
+ * can't contain toasted fields, so instead this routine should be called
+ * when such a HeapTuple is being converted into a Datum.
*
- * If a Datum is of composite type, "flatten" it to contain no toasted fields.
- * This must be invoked on any potentially-composite field that is to be
- * inserted into a tuple. Doing this preserves the invariant that toasting
- * goes only one level deep in a tuple.
+ * While we're at it, we decompress any compressed fields too. This is not
+ * necessary for correctness, but reflects an expectation that compression
+ * will be more effective if applied to the whole tuple not individual
+ * fields. We are not so concerned about that that we want to deconstruct
+ * and reconstruct tuples just to get rid of compressed fields, however.
+ * So callers typically won't call this unless they see that the tuple has
+ * at least one external field.
*
- * Note that flattening does not mean expansion of short-header varlenas,
- * so in one sense toasting is allowed within composite datums.
+ * On the other hand, in-line short-header varlena fields are left alone.
+ * If we "untoasted" them here, they'd just get changed back to short-header
+ * format anyway within heap_fill_tuple.
* ----------
*/
Datum
-toast_flatten_tuple_attribute(Datum value,
- Oid typeId, int32 typeMod)
+toast_flatten_tuple_to_datum(HeapTupleHeader tup,
+ uint32 tup_len,
+ TupleDesc tupleDesc)
{
- TupleDesc tupleDesc;
- HeapTupleHeader olddata;
HeapTupleHeader new_data;
int32 new_header_len;
int32 new_data_len;
int32 new_tuple_len;
HeapTupleData tmptup;
- Form_pg_attribute *att;
- int numAttrs;
+ Form_pg_attribute *att = tupleDesc->attrs;
+ int numAttrs = tupleDesc->natts;
int i;
- bool need_change = false;
bool has_nulls = false;
Datum toast_values[MaxTupleAttributeNumber];
bool toast_isnull[MaxTupleAttributeNumber];
bool toast_free[MaxTupleAttributeNumber];
- /*
- * See if it's a composite type, and get the tupdesc if so.
- */
- tupleDesc = lookup_rowtype_tupdesc_noerror(typeId, typeMod, true);
- if (tupleDesc == NULL)
- return value; /* not a composite type */
-
- att = tupleDesc->attrs;
- numAttrs = tupleDesc->natts;
-
- /*
- * Break down the tuple into fields.
- */
- olddata = DatumGetHeapTupleHeader(value);
- Assert(typeId == HeapTupleHeaderGetTypeId(olddata));
- Assert(typeMod == HeapTupleHeaderGetTypMod(olddata));
/* Build a temporary HeapTuple control structure */
- tmptup.t_len = HeapTupleHeaderGetDatumLength(olddata);
+ tmptup.t_len = tup_len;
ItemPointerSetInvalid(&(tmptup.t_self));
tmptup.t_tableOid = InvalidOid;
- tmptup.t_data = olddata;
+ tmptup.t_data = tup;
+ /*
+ * Break down the tuple into fields.
+ */
Assert(numAttrs <= MaxTupleAttributeNumber);
heap_deform_tuple(&tmptup, tupleDesc, toast_values, toast_isnull);
new_value = heap_tuple_untoast_attr(new_value);
toast_values[i] = PointerGetDatum(new_value);
toast_free[i] = true;
- need_change = true;
}
}
}
- /*
- * If nothing to untoast, just return the original tuple.
- */
- if (!need_change)
- {
- ReleaseTupleDesc(tupleDesc);
- return value;
- }
-
/*
* Calculate the new size of the tuple.
*
new_header_len = offsetof(HeapTupleHeaderData, t_bits);
if (has_nulls)
new_header_len += BITMAPLEN(numAttrs);
- if (olddata->t_infomask & HEAP_HASOID)
+ if (tup->t_infomask & HEAP_HASOID)
new_header_len += sizeof(Oid);
new_header_len = MAXALIGN(new_header_len);
new_data_len = heap_compute_data_size(tupleDesc,
/*
* Copy the existing tuple header, but adjust natts and t_hoff.
*/
- memcpy(new_data, olddata, offsetof(HeapTupleHeaderData, t_bits));
+ memcpy(new_data, tup, offsetof(HeapTupleHeaderData, t_bits));
HeapTupleHeaderSetNatts(new_data, numAttrs);
new_data->t_hoff = new_header_len;
- if (olddata->t_infomask & HEAP_HASOID)
- HeapTupleHeaderSetOid(new_data, HeapTupleHeaderGetOid(olddata));
+ if (tup->t_infomask & HEAP_HASOID)
+ HeapTupleHeaderSetOid(new_data, HeapTupleHeaderGetOid(tup));
- /* Reset the datum length field, too */
+ /* Set the composite-Datum header fields correctly */
HeapTupleHeaderSetDatumLength(new_data, new_tuple_len);
+ HeapTupleHeaderSetTypeId(new_data, tupleDesc->tdtypeid);
+ HeapTupleHeaderSetTypMod(new_data, tupleDesc->tdtypmod);
/* Copy over the data, and fill the null bitmap if needed */
heap_fill_tuple(tupleDesc,
for (i = 0; i < numAttrs; i++)
if (toast_free[i])
pfree(DatumGetPointer(toast_values[i]));
- ReleaseTupleDesc(tupleDesc);
return PointerGetDatum(new_data);
}
{
Var *variable = (Var *) wrvstate->xprstate.expr;
TupleTableSlot *slot;
- HeapTuple tuple;
- TupleDesc tupleDesc;
HeapTupleHeader dtuple;
if (isDone)
if (wrvstate->wrv_junkFilter != NULL)
slot = ExecFilterJunk(wrvstate->wrv_junkFilter, slot);
- tuple = ExecFetchSlotTuple(slot);
- tupleDesc = slot->tts_tupleDescriptor;
-
/*
- * We have to make a copy of the tuple so we can safely insert the Datum
- * overhead fields, which are not set in on-disk tuples.
+ * Copy the slot tuple and make sure any toasted fields get detoasted.
*/
- dtuple = (HeapTupleHeader) palloc(tuple->t_len);
- memcpy((char *) dtuple, (char *) tuple->t_data, tuple->t_len);
-
- HeapTupleHeaderSetDatumLength(dtuple, tuple->t_len);
+ dtuple = DatumGetHeapTupleHeader(ExecFetchSlotTupleDatum(slot));
/*
- * If the Var identifies a named composite type, label the tuple with that
- * type; otherwise use what is in the tupleDesc.
+ * If the Var identifies a named composite type, label the datum with that
+ * type; otherwise we'll use the slot's info.
*/
if (variable->vartype != RECORDOID)
{
HeapTupleHeaderSetTypeId(dtuple, variable->vartype);
HeapTupleHeaderSetTypMod(dtuple, variable->vartypmod);
}
- else
- {
- HeapTupleHeaderSetTypeId(dtuple, tupleDesc->tdtypeid);
- HeapTupleHeaderSetTypMod(dtuple, tupleDesc->tdtypmod);
- }
return PointerGetDatum(dtuple);
}
}
/*
- * We have to make a copy of the tuple so we can safely insert the Datum
- * overhead fields, which are not set in on-disk tuples.
+ * Copy the slot tuple and make sure any toasted fields get detoasted.
*/
- dtuple = (HeapTupleHeader) palloc(tuple->t_len);
- memcpy((char *) dtuple, (char *) tuple->t_data, tuple->t_len);
+ dtuple = DatumGetHeapTupleHeader(ExecFetchSlotTupleDatum(slot));
- HeapTupleHeaderSetDatumLength(dtuple, tuple->t_len);
+ /*
+ * Reset datum's type ID fields to match the Var.
+ */
HeapTupleHeaderSetTypeId(dtuple, variable->vartype);
HeapTupleHeaderSetTypMod(dtuple, variable->vartypmod);
*/
#include "postgres.h"
+#include "access/tuptoaster.h"
#include "funcapi.h"
#include "catalog/pg_type.h"
#include "nodes/nodeFuncs.h"
* ExecFetchSlotTupleDatum
* Fetch the slot's tuple as a composite-type Datum.
*
- * We convert the slot's contents to local physical-tuple form,
- * and fill in the Datum header fields. Note that the result
- * always points to storage owned by the slot.
+ * The result is always freshly palloc'd in the caller's memory context.
* --------------------------------
*/
Datum
ExecFetchSlotTupleDatum(TupleTableSlot *slot)
{
HeapTuple tup;
- HeapTupleHeader td;
TupleDesc tupdesc;
- /* Make sure we can scribble on the slot contents ... */
- tup = ExecMaterializeSlot(slot);
- /* ... and set up the composite-Datum header fields, in case not done */
- td = tup->t_data;
+ /* Fetch slot's contents in regular-physical-tuple form */
+ tup = ExecFetchSlotTuple(slot);
tupdesc = slot->tts_tupleDescriptor;
- HeapTupleHeaderSetDatumLength(td, tup->t_len);
- HeapTupleHeaderSetTypeId(td, tupdesc->tdtypeid);
- HeapTupleHeaderSetTypMod(td, tupdesc->tdtypmod);
- return PointerGetDatum(td);
+
+ /* Convert to Datum form */
+ return heap_copy_tuple_as_datum(tup, tupdesc);
}
/* --------------------------------
return tuple;
}
+/*
+ * HeapTupleHeaderGetDatum - convert a HeapTupleHeader pointer to a Datum.
+ *
+ * This must *not* get applied to an on-disk tuple; the tuple should be
+ * freshly made by heap_form_tuple or some wrapper routine for it (such as
+ * BuildTupleFromCStrings). Be sure also that the tupledesc used to build
+ * the tuple has a properly "blessed" rowtype.
+ *
+ * Formerly this was a macro equivalent to PointerGetDatum, relying on the
+ * fact that heap_form_tuple fills in the appropriate tuple header fields
+ * for a composite Datum. However, we now require that composite Datums not
+ * contain any external TOAST pointers. We do not want heap_form_tuple itself
+ * to enforce that; more specifically, the rule applies only to actual Datums
+ * and not to HeapTuple structures. Therefore, HeapTupleHeaderGetDatum is
+ * now a function that detects whether there are externally-toasted fields
+ * and constructs a new tuple with inlined fields if so. We still need
+ * heap_form_tuple to insert the Datum header fields, because otherwise this
+ * code would have no way to obtain a tupledesc for the tuple.
+ *
+ * Note that if we do build a new tuple, it's palloc'd in the current
+ * memory context. Beware of code that changes context between the initial
+ * heap_form_tuple/etc call and calling HeapTuple(Header)GetDatum.
+ *
+ * For performance-critical callers, it could be worthwhile to take extra
+ * steps to ensure that there aren't TOAST pointers in the output of
+ * heap_form_tuple to begin with. It's likely however that the costs of the
+ * typcache lookup and tuple disassembly/reassembly are swamped by TOAST
+ * dereference costs, so that the benefits of such extra effort would be
+ * minimal.
+ *
+ * XXX it would likely be better to create wrapper functions that produce
+ * a composite Datum from the field values in one step. However, there's
+ * enough code using the existing APIs that we couldn't get rid of this
+ * hack anytime soon.
+ */
+Datum
+HeapTupleHeaderGetDatum(HeapTupleHeader tuple)
+{
+ Datum result;
+ TupleDesc tupDesc;
+
+ /* No work if there are no external TOAST pointers in the tuple */
+ if (!HeapTupleHeaderHasExternal(tuple))
+ return PointerGetDatum(tuple);
+
+ /* Use the type data saved by heap_form_tuple to look up the rowtype */
+ tupDesc = lookup_rowtype_tupdesc(HeapTupleHeaderGetTypeId(tuple),
+ HeapTupleHeaderGetTypMod(tuple));
+
+ /* And do the flattening */
+ result = toast_flatten_tuple_to_datum(tuple,
+ HeapTupleHeaderGetDatumLength(tuple),
+ tupDesc);
+
+ ReleaseTupleDesc(tupDesc);
+
+ return result;
+}
+
+
/*
* Functions for sending tuples to the frontend (or other specified destination)
* as though it is a SELECT result. These are used by utility commands that
/* We must return the whole tuple as a Datum. */
fcinfo->isnull = false;
value = ExecFetchSlotTupleDatum(slot);
- value = datumCopy(value, fcache->typbyval, fcache->typlen);
}
else
{
oldcxt = MemoryContextSwitchTo(_SPI_current->savedcxt);
}
- dtup = (HeapTupleHeader) palloc(tuple->t_len);
- memcpy((char *) dtup, (char *) tuple->t_data, tuple->t_len);
-
- HeapTupleHeaderSetDatumLength(dtup, tuple->t_len);
- HeapTupleHeaderSetTypeId(dtup, tupdesc->tdtypeid);
- HeapTupleHeaderSetTypMod(dtup, tupdesc->tdtypmod);
+ dtup = DatumGetHeapTupleHeader(heap_copy_tuple_as_datum(tuple, tupdesc));
if (oldcxt)
MemoryContextSwitchTo(oldcxt);
#include <ctype.h>
#include "catalog/pg_type.h"
+#include "funcapi.h"
#include "libpq/pqformat.h"
#include "utils/builtins.h"
#include "utils/lsyscache.h"
(tup)->t_infomask2 = ((tup)->t_infomask2 & ~HEAP_NATTS_MASK) | (natts) \
)
+#define HeapTupleHeaderHasExternal(tup) \
+ (((tup)->t_infomask & HEAP_HASEXTERNAL) != 0)
+
/*
* BITMAPLEN(NATTS) -
bool *isnull);
extern HeapTuple heap_copytuple(HeapTuple tuple);
extern void heap_copytuple_with_tuple(HeapTuple src, HeapTuple dest);
+extern Datum heap_copy_tuple_as_datum(HeapTuple tuple, TupleDesc tupleDesc);
extern HeapTuple heap_form_tuple(TupleDesc tupleDescriptor,
Datum *values, bool *isnull);
extern HeapTuple heap_modify_tuple(HeapTuple tuple,
extern HeapTuple toast_flatten_tuple(HeapTuple tup, TupleDesc tupleDesc);
/* ----------
- * toast_flatten_tuple_attribute -
+ * toast_flatten_tuple_to_datum -
*
- * If a Datum is of composite type, "flatten" it to contain no toasted fields.
- * This must be invoked on any potentially-composite field that is to be
- * inserted into a tuple. Doing this preserves the invariant that toasting
- * goes only one level deep in a tuple.
+ * "Flatten" a tuple containing out-of-line toasted fields into a Datum.
* ----------
*/
-extern Datum toast_flatten_tuple_attribute(Datum value,
- Oid typeId, int32 typeMod);
+extern Datum toast_flatten_tuple_to_datum(HeapTupleHeader tup,
+ uint32 tup_len,
+ TupleDesc tupleDesc);
/* ----------
* toast_compress_datum -
#define PG_RETURN_TEXT_P(x) PG_RETURN_POINTER(x)
#define PG_RETURN_BPCHAR_P(x) PG_RETURN_POINTER(x)
#define PG_RETURN_VARCHAR_P(x) PG_RETURN_POINTER(x)
-#define PG_RETURN_HEAPTUPLEHEADER(x) PG_RETURN_POINTER(x)
+#define PG_RETURN_HEAPTUPLEHEADER(x) return HeapTupleHeaderGetDatum(x)
/*-------------------------------------------------------------------------
* HeapTuple BuildTupleFromCStrings(AttInMetadata *attinmeta, char **values) -
* build a HeapTuple given user data in C string form. values is an array
* of C strings, one for each attribute of the return tuple.
+ * Datum HeapTupleHeaderGetDatum(HeapTupleHeader tuple) - convert a
+ * HeapTupleHeader to a Datum.
*
* Macro declarations:
* HeapTupleGetDatum(HeapTuple tuple) - convert a HeapTuple to a Datum.
*----------
*/
-#define HeapTupleGetDatum(_tuple) PointerGetDatum((_tuple)->t_data)
+#define HeapTupleGetDatum(tuple) HeapTupleHeaderGetDatum((tuple)->t_data)
/* obsolete version of above */
-#define TupleGetDatum(_slot, _tuple) PointerGetDatum((_tuple)->t_data)
+#define TupleGetDatum(_slot, _tuple) HeapTupleGetDatum(_tuple)
extern TupleDesc RelationNameGetTupleDesc(const char *relname);
extern TupleDesc TypeGetTupleDesc(Oid typeoid, List *colaliases);
extern TupleDesc BlessTupleDesc(TupleDesc tupdesc);
extern AttInMetadata *TupleDescGetAttInMetadata(TupleDesc tupdesc);
extern HeapTuple BuildTupleFromCStrings(AttInMetadata *attinmeta, char **values);
+extern Datum HeapTupleHeaderGetDatum(HeapTupleHeader tuple);
extern TupleTableSlot *TupleDescGetSlot(TupleDesc tupdesc);
tup = make_tuple_from_row(estate, row, row->rowtupdesc);
if (tup == NULL) /* should not happen */
elog(ERROR, "row not compatible with its own tupdesc");
- MemoryContextSwitchTo(oldcontext);
*typeid = row->rowtupdesc->tdtypeid;
*typetypmod = row->rowtupdesc->tdtypmod;
*value = HeapTupleGetDatum(tup);
*isnull = false;
+ MemoryContextSwitchTo(oldcontext);
break;
}
case PLPGSQL_DTYPE_REC:
{
PLpgSQL_rec *rec = (PLpgSQL_rec *) datum;
- HeapTupleData worktup;
if (!HeapTupleIsValid(rec->tup))
ereport(ERROR,
/* Make sure we have a valid type/typmod setting */
BlessTupleDesc(rec->tupdesc);
- /*
- * In a trigger, the NEW and OLD parameters are likely to be
- * on-disk tuples that don't have the desired Datum fields.
- * Copy the tuple body and insert the right values.
- */
oldcontext = MemoryContextSwitchTo(estate->eval_econtext->ecxt_per_tuple_memory);
- heap_copytuple_with_tuple(rec->tup, &worktup);
- HeapTupleHeaderSetDatumLength(worktup.t_data, worktup.t_len);
- HeapTupleHeaderSetTypeId(worktup.t_data, rec->tupdesc->tdtypeid);
- HeapTupleHeaderSetTypMod(worktup.t_data, rec->tupdesc->tdtypmod);
- MemoryContextSwitchTo(oldcontext);
*typeid = rec->tupdesc->tdtypeid;
*typetypmod = rec->tupdesc->tdtypmod;
- *value = HeapTupleGetDatum(&worktup);
+ *value = heap_copy_tuple_as_datum(rec->tup, rec->tupdesc);
*isnull = false;
+ MemoryContextSwitchTo(oldcontext);
break;
}
[5:5]={"(42,43)"}
(1 row)
+-- Check that arrays of composites are safely detoasted when needed
+create temp table src (f1 text);
+insert into src
+ select string_agg(random()::text,'') from generate_series(1,10000);
+create type textandtext as (c1 text, c2 text);
+create temp table dest (f1 textandtext[]);
+insert into dest select array[row(f1,f1)::textandtext] from src;
+select length(md5((f1[1]).c2)) from dest;
+ length
+--------
+ 32
+(1 row)
+
+delete from src;
+select length(md5((f1[1]).c2)) from dest;
+ length
+--------
+ 32
+(1 row)
+
+truncate table src;
+drop table src;
+select length(md5((f1[1]).c2)) from dest;
+ length
+--------
+ 32
+(1 row)
+
+drop table dest;
+drop type textandtext;
select * from t1;
update t1 set f1[5].q2 = 43;
select * from t1;
+
+-- Check that arrays of composites are safely detoasted when needed
+
+create temp table src (f1 text);
+insert into src
+ select string_agg(random()::text,'') from generate_series(1,10000);
+create type textandtext as (c1 text, c2 text);
+create temp table dest (f1 textandtext[]);
+insert into dest select array[row(f1,f1)::textandtext] from src;
+select length(md5((f1[1]).c2)) from dest;
+delete from src;
+select length(md5((f1[1]).c2)) from dest;
+truncate table src;
+drop table src;
+select length(md5((f1[1]).c2)) from dest;
+drop table dest;
+drop type textandtext;