and they can return a result of any of these types. They can also
accept or return any composite type (row type) specified by name.
It is also possible to declare a <application>PL/pgSQL</application>
- function as returning <type>record</type>, which means that the result
+ function as accepting <type>record</type>, which means that any
+ composite type will do as input, or
+ as returning <type>record</type>, which means that the result
is a row type whose columns are determined by specification in the
calling query, as discussed in <xref linkend="queries-tablefunctions"/>.
</para>
be selected from it, for example <literal>$1.user_id</literal>.
</para>
- <para>
- Only the user-defined columns of a table row are accessible in a
- row-type variable, not the OID or other system columns (because the
- row could be from a view). The fields of the row type inherit the
- table's field size or precision for data types such as
- <type>char(<replaceable>n</replaceable>)</type>.
- </para>
-
<para>
Here is an example of using composite types. <structname>table1</structname>
and <structname>table2</structname> are existing tables having at least the
#include "utils/builtins.h"
#include "utils/date.h"
#include "utils/datum.h"
+#include "utils/expandedrecord.h"
#include "utils/lsyscache.h"
#include "utils/timestamp.h"
#include "utils/typcache.h"
if (*op->resnull)
return;
- /* Get the composite datum and extract its type fields */
tupDatum = *op->resvalue;
- tuple = DatumGetHeapTupleHeader(tupDatum);
- tupType = HeapTupleHeaderGetTypeId(tuple);
- tupTypmod = HeapTupleHeaderGetTypMod(tuple);
+ /* We can special-case expanded records for speed */
+ if (VARATT_IS_EXTERNAL_EXPANDED(DatumGetPointer(tupDatum)))
+ {
+ ExpandedRecordHeader *erh = (ExpandedRecordHeader *) DatumGetEOHP(tupDatum);
- /* Lookup tupdesc if first time through or if type changes */
- tupDesc = get_cached_rowtype(tupType, tupTypmod,
- &op->d.fieldselect.argdesc,
- econtext);
+ Assert(erh->er_magic == ER_MAGIC);
- /*
- * Find field's attr record. Note we don't support system columns here: a
- * datum tuple doesn't have valid values for most of the interesting
- * system columns anyway.
- */
- if (fieldnum <= 0) /* should never happen */
- elog(ERROR, "unsupported reference to system column %d in FieldSelect",
- fieldnum);
- if (fieldnum > tupDesc->natts) /* should never happen */
- elog(ERROR, "attribute number %d exceeds number of columns %d",
- fieldnum, tupDesc->natts);
- attr = TupleDescAttr(tupDesc, fieldnum - 1);
-
- /* Check for dropped column, and force a NULL result if so */
- if (attr->attisdropped)
- {
- *op->resnull = true;
- return;
+ /* Extract record's TupleDesc */
+ tupDesc = expanded_record_get_tupdesc(erh);
+
+ /*
+ * Find field's attr record. Note we don't support system columns
+ * here: a datum tuple doesn't have valid values for most of the
+ * interesting system columns anyway.
+ */
+ if (fieldnum <= 0) /* should never happen */
+ elog(ERROR, "unsupported reference to system column %d in FieldSelect",
+ fieldnum);
+ if (fieldnum > tupDesc->natts) /* should never happen */
+ elog(ERROR, "attribute number %d exceeds number of columns %d",
+ fieldnum, tupDesc->natts);
+ attr = TupleDescAttr(tupDesc, fieldnum - 1);
+
+ /* Check for dropped column, and force a NULL result if so */
+ if (attr->attisdropped)
+ {
+ *op->resnull = true;
+ return;
+ }
+
+ /* Check for type mismatch --- possible after ALTER COLUMN TYPE? */
+ /* As in CheckVarSlotCompatibility, we should but can't check typmod */
+ if (op->d.fieldselect.resulttype != attr->atttypid)
+ ereport(ERROR,
+ (errcode(ERRCODE_DATATYPE_MISMATCH),
+ errmsg("attribute %d has wrong type", fieldnum),
+ errdetail("Table has type %s, but query expects %s.",
+ format_type_be(attr->atttypid),
+ format_type_be(op->d.fieldselect.resulttype))));
+
+ /* extract the field */
+ *op->resvalue = expanded_record_get_field(erh, fieldnum,
+ op->resnull);
}
+ else
+ {
+ /* Get the composite datum and extract its type fields */
+ tuple = DatumGetHeapTupleHeader(tupDatum);
- /* Check for type mismatch --- possible after ALTER COLUMN TYPE? */
- /* As in CheckVarSlotCompatibility, we should but can't check typmod */
- if (op->d.fieldselect.resulttype != attr->atttypid)
- ereport(ERROR,
- (errcode(ERRCODE_DATATYPE_MISMATCH),
- errmsg("attribute %d has wrong type", fieldnum),
- errdetail("Table has type %s, but query expects %s.",
- format_type_be(attr->atttypid),
- format_type_be(op->d.fieldselect.resulttype))));
+ tupType = HeapTupleHeaderGetTypeId(tuple);
+ tupTypmod = HeapTupleHeaderGetTypMod(tuple);
- /* heap_getattr needs a HeapTuple not a bare HeapTupleHeader */
- tmptup.t_len = HeapTupleHeaderGetDatumLength(tuple);
- tmptup.t_data = tuple;
+ /* Lookup tupdesc if first time through or if type changes */
+ tupDesc = get_cached_rowtype(tupType, tupTypmod,
+ &op->d.fieldselect.argdesc,
+ econtext);
+
+ /*
+ * Find field's attr record. Note we don't support system columns
+ * here: a datum tuple doesn't have valid values for most of the
+ * interesting system columns anyway.
+ */
+ if (fieldnum <= 0) /* should never happen */
+ elog(ERROR, "unsupported reference to system column %d in FieldSelect",
+ fieldnum);
+ if (fieldnum > tupDesc->natts) /* should never happen */
+ elog(ERROR, "attribute number %d exceeds number of columns %d",
+ fieldnum, tupDesc->natts);
+ attr = TupleDescAttr(tupDesc, fieldnum - 1);
- /* extract the field */
- *op->resvalue = heap_getattr(&tmptup,
- fieldnum,
- tupDesc,
- op->resnull);
+ /* Check for dropped column, and force a NULL result if so */
+ if (attr->attisdropped)
+ {
+ *op->resnull = true;
+ return;
+ }
+
+ /* Check for type mismatch --- possible after ALTER COLUMN TYPE? */
+ /* As in CheckVarSlotCompatibility, we should but can't check typmod */
+ if (op->d.fieldselect.resulttype != attr->atttypid)
+ ereport(ERROR,
+ (errcode(ERRCODE_DATATYPE_MISMATCH),
+ errmsg("attribute %d has wrong type", fieldnum),
+ errdetail("Table has type %s, but query expects %s.",
+ format_type_be(attr->atttypid),
+ format_type_be(op->d.fieldselect.resulttype))));
+
+ /* heap_getattr needs a HeapTuple not a bare HeapTupleHeader */
+ tmptup.t_len = HeapTupleHeaderGetDatumLength(tuple);
+ tmptup.t_data = tuple;
+
+ /* extract the field */
+ *op->resvalue = heap_getattr(&tmptup,
+ fieldnum,
+ tupDesc,
+ op->resnull);
+ }
}
/*
OBJS = acl.o amutils.o arrayfuncs.o array_expanded.o array_selfuncs.o \
array_typanalyze.o array_userfuncs.o arrayutils.o ascii.o \
bool.o cash.o char.o date.o datetime.o datum.o dbsize.o domains.o \
- encode.o enum.o expandeddatum.o \
+ encode.o enum.o expandeddatum.o expandedrecord.o \
float.o format_type.o formatting.o genfile.o \
geo_ops.o geo_selfuncs.o geo_spgist.o inet_cidr_ntop.o inet_net_pton.o \
int.o int8.o json.o jsonb.o jsonb_gin.o jsonb_op.o jsonb_util.o \
--- /dev/null
+/*-------------------------------------------------------------------------
+ *
+ * expandedrecord.c
+ * Functions for manipulating composite expanded objects.
+ *
+ * This module supports "expanded objects" (cf. expandeddatum.h) that can
+ * store values of named composite types, domains over named composite types,
+ * and record types (registered or anonymous).
+ *
+ * Portions Copyright (c) 1996-2018, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ *
+ * IDENTIFICATION
+ * src/backend/utils/adt/expandedrecord.c
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include "access/htup_details.h"
+#include "catalog/heap.h"
+#include "catalog/pg_type.h"
+#include "utils/builtins.h"
+#include "utils/datum.h"
+#include "utils/expandedrecord.h"
+#include "utils/memutils.h"
+#include "utils/typcache.h"
+
+
+/* "Methods" required for an expanded object */
+static Size ER_get_flat_size(ExpandedObjectHeader *eohptr);
+static void ER_flatten_into(ExpandedObjectHeader *eohptr,
+ void *result, Size allocated_size);
+
+static const ExpandedObjectMethods ER_methods =
+{
+ ER_get_flat_size,
+ ER_flatten_into
+};
+
+/* Other local functions */
+static void ER_mc_callback(void *arg);
+static MemoryContext get_domain_check_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,
+ Datum newValue, bool isnull);
+static pg_noinline void check_domain_for_new_tuple(ExpandedRecordHeader *erh,
+ HeapTuple tuple);
+
+
+/*
+ * Build an expanded record of the specified composite type
+ *
+ * type_id can be RECORDOID, but only if a positive typmod is given.
+ *
+ * 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).
+ *
+ * The expanded object will be a child of parentcontext.
+ */
+ExpandedRecordHeader *
+make_expanded_record_from_typeid(Oid type_id, int32 typmod,
+ MemoryContext parentcontext)
+{
+ ExpandedRecordHeader *erh;
+ int flags = 0;
+ TupleDesc tupdesc;
+ uint64 tupdesc_id;
+ MemoryContext objcxt;
+ char *chunk;
+
+ if (type_id != RECORDOID)
+ {
+ /*
+ * Consult the typcache to see if it's a domain over composite, and in
+ * any case to get the tupdesc and tupdesc identifier.
+ */
+ TypeCacheEntry *typentry;
+
+ typentry = lookup_type_cache(type_id,
+ TYPECACHE_TUPDESC |
+ TYPECACHE_DOMAIN_BASE_INFO);
+ if (typentry->typtype == TYPTYPE_DOMAIN)
+ {
+ flags |= ER_FLAG_IS_DOMAIN;
+ typentry = lookup_type_cache(typentry->domainBaseType,
+ TYPECACHE_TUPDESC);
+ }
+ if (typentry->tupDesc == NULL)
+ ereport(ERROR,
+ (errcode(ERRCODE_WRONG_OBJECT_TYPE),
+ errmsg("type %s is not composite",
+ format_type_be(type_id))));
+ tupdesc = typentry->tupDesc;
+ tupdesc_id = typentry->tupDesc_identifier;
+ }
+ else
+ {
+ /*
+ * For RECORD types, get the tupdesc and identifier from typcache.
+ */
+ tupdesc = lookup_rowtype_tupdesc(type_id, typmod);
+ tupdesc_id = assign_record_type_identifier(type_id, typmod);
+ }
+
+ /*
+ * Allocate private context for expanded object. We use a regular-size
+ * context, not a small one, to improve the odds that we can fit a tupdesc
+ * into it without needing an extra malloc block. (This code path doesn't
+ * ever need to copy a tupdesc into the expanded record, but let's be
+ * consistent with the other ways of making an expanded record.)
+ */
+ objcxt = AllocSetContextCreate(parentcontext,
+ "expanded record",
+ ALLOCSET_DEFAULT_SIZES);
+
+ /*
+ * Since we already know the number of fields in the tupdesc, we can
+ * allocate the dvalues/dnulls arrays along with the record header. This
+ * is useless if we never need those arrays, but it costs almost nothing,
+ * and it will save a palloc cycle if we do need them.
+ */
+ erh = (ExpandedRecordHeader *)
+ MemoryContextAlloc(objcxt, MAXALIGN(sizeof(ExpandedRecordHeader))
+ + tupdesc->natts * (sizeof(Datum) + sizeof(bool)));
+
+ /* Ensure all header fields are initialized to 0/null */
+ memset(erh, 0, sizeof(ExpandedRecordHeader));
+
+ EOH_init_header(&erh->hdr, &ER_methods, objcxt);
+ erh->er_magic = ER_MAGIC;
+
+ /* Set up dvalues/dnulls, with no valid contents as yet */
+ chunk = (char *) erh + MAXALIGN(sizeof(ExpandedRecordHeader));
+ erh->dvalues = (Datum *) chunk;
+ erh->dnulls = (bool *) (chunk + tupdesc->natts * sizeof(Datum));
+ erh->nfields = tupdesc->natts;
+
+ /* Fill in composite-type identification info */
+ erh->er_decltypeid = type_id;
+ erh->er_typeid = tupdesc->tdtypeid;
+ erh->er_typmod = tupdesc->tdtypmod;
+ erh->er_tupdesc_id = tupdesc_id;
+
+ erh->flags = flags;
+
+ /*
+ * If what we got from the typcache is a refcounted tupdesc, we need to
+ * acquire our own refcount on it. We manage the refcount with a memory
+ * context callback rather than assuming that the CurrentResourceOwner is
+ * longer-lived than this expanded object.
+ */
+ if (tupdesc->tdrefcount >= 0)
+ {
+ /* Register callback to release the refcount */
+ erh->er_mcb.func = ER_mc_callback;
+ erh->er_mcb.arg = (void *) erh;
+ MemoryContextRegisterResetCallback(erh->hdr.eoh_context,
+ &erh->er_mcb);
+
+ /* And save the pointer */
+ erh->er_tupdesc = tupdesc;
+ tupdesc->tdrefcount++;
+
+ /* If we called lookup_rowtype_tupdesc, release the pin it took */
+ if (type_id == RECORDOID)
+ DecrTupleDescRefCount(tupdesc);
+ }
+ else
+ {
+ /*
+ * If it's not refcounted, just assume it will outlive the expanded
+ * object. (This can happen for shared record types, for instance.)
+ */
+ erh->er_tupdesc = tupdesc;
+ }
+
+ /*
+ * We don't set ER_FLAG_DVALUES_VALID or ER_FLAG_FVALUE_VALID, so the
+ * record remains logically empty.
+ */
+
+ return erh;
+}
+
+/*
+ * Build an expanded record of the rowtype defined by the tupdesc
+ *
+ * The tupdesc is copied if necessary (i.e., if we can't just bump its
+ * reference count instead).
+ *
+ * The expanded record is initially "empty", having a state logically
+ * equivalent to a NULL composite value (not ROW(NULL, NULL, ...)).
+ *
+ * The expanded object will be a child of parentcontext.
+ */
+ExpandedRecordHeader *
+make_expanded_record_from_tupdesc(TupleDesc tupdesc,
+ MemoryContext parentcontext)
+{
+ ExpandedRecordHeader *erh;
+ uint64 tupdesc_id;
+ MemoryContext objcxt;
+ MemoryContext oldcxt;
+ char *chunk;
+
+ if (tupdesc->tdtypeid != RECORDOID)
+ {
+ /*
+ * If it's a named composite type (not RECORD), we prefer to reference
+ * the typcache's copy of the tupdesc, which is guaranteed to be
+ * refcounted (the given tupdesc might not be). In any case, we need
+ * to consult the typcache to get the correct tupdesc identifier.
+ *
+ * Note that tdtypeid couldn't be a domain type, so we need not
+ * consider that case here.
+ */
+ TypeCacheEntry *typentry;
+
+ typentry = lookup_type_cache(tupdesc->tdtypeid, TYPECACHE_TUPDESC);
+ if (typentry->tupDesc == NULL)
+ ereport(ERROR,
+ (errcode(ERRCODE_WRONG_OBJECT_TYPE),
+ errmsg("type %s is not composite",
+ format_type_be(tupdesc->tdtypeid))));
+ tupdesc = typentry->tupDesc;
+ tupdesc_id = typentry->tupDesc_identifier;
+ }
+ else
+ {
+ /*
+ * For RECORD types, get the appropriate unique identifier (possibly
+ * freshly assigned).
+ */
+ tupdesc_id = assign_record_type_identifier(tupdesc->tdtypeid,
+ tupdesc->tdtypmod);
+ }
+
+ /*
+ * Allocate private context for expanded object. We use a regular-size
+ * context, not a small one, to improve the odds that we can fit a tupdesc
+ * into it without needing an extra malloc block.
+ */
+ objcxt = AllocSetContextCreate(parentcontext,
+ "expanded record",
+ ALLOCSET_DEFAULT_SIZES);
+
+ /*
+ * Since we already know the number of fields in the tupdesc, we can
+ * allocate the dvalues/dnulls arrays along with the record header. This
+ * is useless if we never need those arrays, but it costs almost nothing,
+ * and it will save a palloc cycle if we do need them.
+ */
+ erh = (ExpandedRecordHeader *)
+ MemoryContextAlloc(objcxt, MAXALIGN(sizeof(ExpandedRecordHeader))
+ + tupdesc->natts * (sizeof(Datum) + sizeof(bool)));
+
+ /* Ensure all header fields are initialized to 0/null */
+ memset(erh, 0, sizeof(ExpandedRecordHeader));
+
+ EOH_init_header(&erh->hdr, &ER_methods, objcxt);
+ erh->er_magic = ER_MAGIC;
+
+ /* Set up dvalues/dnulls, with no valid contents as yet */
+ chunk = (char *) erh + MAXALIGN(sizeof(ExpandedRecordHeader));
+ erh->dvalues = (Datum *) chunk;
+ erh->dnulls = (bool *) (chunk + tupdesc->natts * sizeof(Datum));
+ erh->nfields = tupdesc->natts;
+
+ /* Fill in composite-type identification info */
+ erh->er_decltypeid = erh->er_typeid = tupdesc->tdtypeid;
+ erh->er_typmod = tupdesc->tdtypmod;
+ erh->er_tupdesc_id = tupdesc_id;
+
+ /*
+ * Copy tupdesc if needed, but we prefer to bump its refcount if possible.
+ * We manage the refcount with a memory context callback rather than
+ * assuming that the CurrentResourceOwner is longer-lived than this
+ * expanded object.
+ */
+ if (tupdesc->tdrefcount >= 0)
+ {
+ /* Register callback to release the refcount */
+ erh->er_mcb.func = ER_mc_callback;
+ erh->er_mcb.arg = (void *) erh;
+ MemoryContextRegisterResetCallback(erh->hdr.eoh_context,
+ &erh->er_mcb);
+
+ /* And save the pointer */
+ erh->er_tupdesc = tupdesc;
+ tupdesc->tdrefcount++;
+ }
+ else
+ {
+ /* Just copy it */
+ oldcxt = MemoryContextSwitchTo(objcxt);
+ erh->er_tupdesc = CreateTupleDescCopy(tupdesc);
+ erh->flags |= ER_FLAG_TUPDESC_ALLOCED;
+ MemoryContextSwitchTo(oldcxt);
+ }
+
+ /*
+ * We don't set ER_FLAG_DVALUES_VALID or ER_FLAG_FVALUE_VALID, so the
+ * record remains logically empty.
+ */
+
+ return erh;
+}
+
+/*
+ * Build an expanded record of the same rowtype as the given expanded record
+ *
+ * This is faster than either of the above routines because we can bypass
+ * typcache lookup(s).
+ *
+ * The expanded record is initially "empty" --- we do not copy whatever
+ * tuple might be in the source expanded record.
+ *
+ * The expanded object will be a child of parentcontext.
+ */
+ExpandedRecordHeader *
+make_expanded_record_from_exprecord(ExpandedRecordHeader *olderh,
+ MemoryContext parentcontext)
+{
+ ExpandedRecordHeader *erh;
+ TupleDesc tupdesc = expanded_record_get_tupdesc(olderh);
+ MemoryContext objcxt;
+ MemoryContext oldcxt;
+ char *chunk;
+
+ /*
+ * Allocate private context for expanded object. We use a regular-size
+ * context, not a small one, to improve the odds that we can fit a tupdesc
+ * into it without needing an extra malloc block.
+ */
+ objcxt = AllocSetContextCreate(parentcontext,
+ "expanded record",
+ ALLOCSET_DEFAULT_SIZES);
+
+ /*
+ * Since we already know the number of fields in the tupdesc, we can
+ * allocate the dvalues/dnulls arrays along with the record header. This
+ * is useless if we never need those arrays, but it costs almost nothing,
+ * and it will save a palloc cycle if we do need them.
+ */
+ erh = (ExpandedRecordHeader *)
+ MemoryContextAlloc(objcxt, MAXALIGN(sizeof(ExpandedRecordHeader))
+ + tupdesc->natts * (sizeof(Datum) + sizeof(bool)));
+
+ /* Ensure all header fields are initialized to 0/null */
+ memset(erh, 0, sizeof(ExpandedRecordHeader));
+
+ EOH_init_header(&erh->hdr, &ER_methods, objcxt);
+ erh->er_magic = ER_MAGIC;
+
+ /* Set up dvalues/dnulls, with no valid contents as yet */
+ chunk = (char *) erh + MAXALIGN(sizeof(ExpandedRecordHeader));
+ erh->dvalues = (Datum *) chunk;
+ erh->dnulls = (bool *) (chunk + tupdesc->natts * sizeof(Datum));
+ erh->nfields = tupdesc->natts;
+
+ /* Fill in composite-type identification info */
+ erh->er_decltypeid = olderh->er_decltypeid;
+ erh->er_typeid = olderh->er_typeid;
+ erh->er_typmod = olderh->er_typmod;
+ erh->er_tupdesc_id = olderh->er_tupdesc_id;
+
+ /* The only flag bit that transfers over is IS_DOMAIN */
+ erh->flags = olderh->flags & ER_FLAG_IS_DOMAIN;
+
+ /*
+ * Copy tupdesc if needed, but we prefer to bump its refcount if possible.
+ * We manage the refcount with a memory context callback rather than
+ * assuming that the CurrentResourceOwner is longer-lived than this
+ * expanded object.
+ */
+ if (tupdesc->tdrefcount >= 0)
+ {
+ /* Register callback to release the refcount */
+ erh->er_mcb.func = ER_mc_callback;
+ erh->er_mcb.arg = (void *) erh;
+ MemoryContextRegisterResetCallback(erh->hdr.eoh_context,
+ &erh->er_mcb);
+
+ /* And save the pointer */
+ erh->er_tupdesc = tupdesc;
+ tupdesc->tdrefcount++;
+ }
+ else if (olderh->flags & ER_FLAG_TUPDESC_ALLOCED)
+ {
+ /* We need to make our own copy of the tupdesc */
+ oldcxt = MemoryContextSwitchTo(objcxt);
+ erh->er_tupdesc = CreateTupleDescCopy(tupdesc);
+ erh->flags |= ER_FLAG_TUPDESC_ALLOCED;
+ MemoryContextSwitchTo(oldcxt);
+ }
+ else
+ {
+ /*
+ * Assume the tupdesc will outlive this expanded object, just like
+ * we're assuming it will outlive the source object.
+ */
+ erh->er_tupdesc = tupdesc;
+ }
+
+ /*
+ * We don't set ER_FLAG_DVALUES_VALID or ER_FLAG_FVALUE_VALID, so the
+ * record remains logically empty.
+ */
+
+ return erh;
+}
+
+/*
+ * Insert given tuple as the value of the expanded record
+ *
+ * It is caller's responsibility that the tuple matches the record's
+ * previously-assigned rowtype. (However domain constraints, if any,
+ * will be checked here.)
+ *
+ * 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.
+ *
+ * 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)
+{
+ int oldflags;
+ HeapTuple oldtuple;
+ char *oldfstartptr;
+ char *oldfendptr;
+ int newflags;
+ HeapTuple newtuple;
+ MemoryContext oldcxt;
+
+ /* Shouldn't ever be trying to assign new data to a dummy header */
+ Assert(!(erh->flags & ER_FLAG_IS_DUMMY));
+
+ /*
+ * Before performing the assignment, see if result will satisfy domain.
+ */
+ if (erh->flags & ER_FLAG_IS_DOMAIN)
+ check_domain_for_new_tuple(erh, tuple);
+
+ /*
+ * Initialize new flags, keeping only non-data status bits.
+ */
+ oldflags = erh->flags;
+ newflags = oldflags & ER_FLAGS_NON_DATA;
+
+ /*
+ * Copy tuple into local storage if needed. We must be sure this succeeds
+ * before we start to modify the expanded record's state.
+ */
+ if (copy && tuple)
+ {
+ oldcxt = MemoryContextSwitchTo(erh->hdr.eoh_context);
+ newtuple = heap_copytuple(tuple);
+ newflags |= ER_FLAG_FVALUE_ALLOCED;
+ MemoryContextSwitchTo(oldcxt);
+ }
+ else
+ newtuple = tuple;
+
+ /* Make copies of fields we're about to overwrite */
+ oldtuple = erh->fvalue;
+ oldfstartptr = erh->fstartptr;
+ oldfendptr = erh->fendptr;
+
+ /*
+ * It's now safe to update the expanded record's state.
+ */
+ if (newtuple)
+ {
+ /* Save flat representation */
+ erh->fvalue = newtuple;
+ erh->fstartptr = (char *) newtuple->t_data;
+ erh->fendptr = ((char *) newtuple->t_data) + newtuple->t_len;
+ newflags |= ER_FLAG_FVALUE_VALID;
+
+ /* Remember if we have any out-of-line field values */
+ if (HeapTupleHasExternal(newtuple))
+ newflags |= ER_FLAG_HAVE_EXTERNAL;
+ }
+ else
+ {
+ erh->fvalue = NULL;
+ erh->fstartptr = erh->fendptr = NULL;
+ }
+
+ erh->flags = newflags;
+
+ /* Reset flat-size info; we don't bother to make it valid now */
+ erh->flat_size = 0;
+
+ /*
+ * Now, release any storage belonging to old field values. It's safe to
+ * do this because ER_FLAG_DVALUES_VALID is no longer set in erh->flags;
+ * even if we fail partway through, the record is valid, and at worst
+ * we've failed to reclaim some space.
+ */
+ if (oldflags & ER_FLAG_DVALUES_ALLOCED)
+ {
+ TupleDesc tupdesc = erh->er_tupdesc;
+ int i;
+
+ for (i = 0; i < erh->nfields; i++)
+ {
+ if (!erh->dnulls[i] &&
+ !(TupleDescAttr(tupdesc, i)->attbyval))
+ {
+ char *oldValue = (char *) DatumGetPointer(erh->dvalues[i]);
+
+ if (oldValue < oldfstartptr || oldValue >= oldfendptr)
+ pfree(oldValue);
+ }
+ }
+ }
+
+ /* Likewise free the old tuple, if it was locally allocated */
+ if (oldflags & ER_FLAG_FVALUE_ALLOCED)
+ heap_freetuple(oldtuple);
+
+ /* We won't make a new deconstructed representation until/unless needed */
+}
+
+/*
+ * make_expanded_record_from_datum: build expanded record from composite Datum
+ *
+ * This combines the functions of make_expanded_record_from_typeid and
+ * expanded_record_set_tuple. However, we do not force a lookup of the
+ * tupdesc immediately, reasoning that it might never be needed.
+ *
+ * The expanded object will be a child of parentcontext.
+ *
+ * Note: a composite datum cannot self-identify as being of a domain type,
+ * so we need not consider domain cases here.
+ */
+Datum
+make_expanded_record_from_datum(Datum recorddatum, MemoryContext parentcontext)
+{
+ ExpandedRecordHeader *erh;
+ HeapTupleHeader tuphdr;
+ HeapTupleData tmptup;
+ HeapTuple newtuple;
+ MemoryContext objcxt;
+ MemoryContext oldcxt;
+
+ /*
+ * Allocate private context for expanded object. We use a regular-size
+ * context, not a small one, to improve the odds that we can fit a tupdesc
+ * into it without needing an extra malloc block.
+ */
+ objcxt = AllocSetContextCreate(parentcontext,
+ "expanded record",
+ ALLOCSET_DEFAULT_SIZES);
+
+ /* Set up expanded record header, initializing fields to 0/null */
+ erh = (ExpandedRecordHeader *)
+ MemoryContextAllocZero(objcxt, sizeof(ExpandedRecordHeader));
+
+ EOH_init_header(&erh->hdr, &ER_methods, objcxt);
+ erh->er_magic = ER_MAGIC;
+
+ /*
+ * Detoast and copy source record into private context, as a HeapTuple.
+ * (If we actually have to detoast the source, we'll leak some memory in
+ * the caller's context, but it doesn't seem worth worrying about.)
+ */
+ tuphdr = DatumGetHeapTupleHeader(recorddatum);
+
+ tmptup.t_len = HeapTupleHeaderGetDatumLength(tuphdr);
+ ItemPointerSetInvalid(&(tmptup.t_self));
+ tmptup.t_tableOid = InvalidOid;
+ tmptup.t_data = tuphdr;
+
+ oldcxt = MemoryContextSwitchTo(objcxt);
+ newtuple = heap_copytuple(&tmptup);
+ erh->flags |= ER_FLAG_FVALUE_ALLOCED;
+ MemoryContextSwitchTo(oldcxt);
+
+ /* Fill in composite-type identification info */
+ erh->er_decltypeid = erh->er_typeid = HeapTupleHeaderGetTypeId(tuphdr);
+ erh->er_typmod = HeapTupleHeaderGetTypMod(tuphdr);
+
+ /* remember we have a flat representation */
+ erh->fvalue = newtuple;
+ erh->fstartptr = (char *) newtuple->t_data;
+ erh->fendptr = ((char *) newtuple->t_data) + newtuple->t_len;
+ erh->flags |= ER_FLAG_FVALUE_VALID;
+
+ /* Shouldn't need to set ER_FLAG_HAVE_EXTERNAL */
+ Assert(!HeapTupleHeaderHasExternal(tuphdr));
+
+ /*
+ * We won't look up the tupdesc till we have to, nor make a deconstructed
+ * representation. We don't have enough info to fill flat_size and
+ * friends, either.
+ */
+
+ /* return a R/W pointer to the expanded record */
+ return EOHPGetRWDatum(&erh->hdr);
+}
+
+/*
+ * get_flat_size method for expanded records
+ *
+ * Note: call this in a reasonably short-lived memory context, in case of
+ * memory leaks from activities such as detoasting.
+ */
+static Size
+ER_get_flat_size(ExpandedObjectHeader *eohptr)
+{
+ ExpandedRecordHeader *erh = (ExpandedRecordHeader *) eohptr;
+ TupleDesc tupdesc;
+ Size len;
+ Size data_len;
+ int hoff;
+ bool hasnull;
+ int i;
+
+ Assert(erh->er_magic == ER_MAGIC);
+
+ /*
+ * The flat representation has to be a valid composite datum. Make sure
+ * that we have a registered, not anonymous, RECORD type.
+ */
+ if (erh->er_typeid == RECORDOID &&
+ erh->er_typmod < 0)
+ {
+ tupdesc = expanded_record_get_tupdesc(erh);
+ assign_record_type_typmod(tupdesc);
+ erh->er_typmod = tupdesc->tdtypmod;
+ }
+
+ /*
+ * If we have a valid flattened value without out-of-line fields, we can
+ * just use it as-is.
+ */
+ if (erh->flags & ER_FLAG_FVALUE_VALID &&
+ !(erh->flags & ER_FLAG_HAVE_EXTERNAL))
+ return erh->fvalue->t_len;
+
+ /* If we have a cached size value, believe that */
+ if (erh->flat_size)
+ return erh->flat_size;
+
+ /* If we haven't yet deconstructed the tuple, do that */
+ if (!(erh->flags & ER_FLAG_DVALUES_VALID))
+ deconstruct_expanded_record(erh);
+
+ /* Tuple descriptor must be valid by now */
+ tupdesc = erh->er_tupdesc;
+
+ /*
+ * Composite datums mustn't contain any out-of-line values.
+ */
+ if (erh->flags & ER_FLAG_HAVE_EXTERNAL)
+ {
+ for (i = 0; i < erh->nfields; i++)
+ {
+ Form_pg_attribute attr = TupleDescAttr(tupdesc, i);
+
+ if (!erh->dnulls[i] &&
+ !attr->attbyval && attr->attlen == -1 &&
+ 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.
+ */
+ 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,
+ false);
+ /* Might as well free the detoasted value */
+ pfree(DatumGetPointer(newValue));
+ }
+ }
+
+ /*
+ * We have now removed all external field values, so we can clear the
+ * flag about them. This won't cause ER_flatten_into() to mistakenly
+ * take the fast path, since expanded_record_set_field() will have
+ * cleared ER_FLAG_FVALUE_VALID.
+ */
+ erh->flags &= ~ER_FLAG_HAVE_EXTERNAL;
+ }
+
+ /* Test if we currently have any null values */
+ hasnull = false;
+ for (i = 0; i < erh->nfields; i++)
+ {
+ if (erh->dnulls[i])
+ {
+ hasnull = true;
+ break;
+ }
+ }
+
+ /* Determine total space needed */
+ len = offsetof(HeapTupleHeaderData, t_bits);
+
+ if (hasnull)
+ len += BITMAPLEN(tupdesc->natts);
+
+ if (tupdesc->tdhasoid)
+ len += sizeof(Oid);
+
+ hoff = len = MAXALIGN(len); /* align user data safely */
+
+ data_len = heap_compute_data_size(tupdesc, erh->dvalues, erh->dnulls);
+
+ len += data_len;
+
+ /* Cache for next time */
+ erh->flat_size = len;
+ erh->data_len = data_len;
+ erh->hoff = hoff;
+ erh->hasnull = hasnull;
+
+ return len;
+}
+
+/*
+ * flatten_into method for expanded records
+ */
+static void
+ER_flatten_into(ExpandedObjectHeader *eohptr,
+ void *result, Size allocated_size)
+{
+ ExpandedRecordHeader *erh = (ExpandedRecordHeader *) eohptr;
+ HeapTupleHeader tuphdr = (HeapTupleHeader) result;
+ TupleDesc tupdesc;
+
+ Assert(erh->er_magic == ER_MAGIC);
+
+ /* Easy if we have a valid flattened value without out-of-line fields */
+ if (erh->flags & ER_FLAG_FVALUE_VALID &&
+ !(erh->flags & ER_FLAG_HAVE_EXTERNAL))
+ {
+ Assert(allocated_size == erh->fvalue->t_len);
+ memcpy(tuphdr, erh->fvalue->t_data, allocated_size);
+ /* The original flattened value might not have datum header fields */
+ HeapTupleHeaderSetDatumLength(tuphdr, allocated_size);
+ HeapTupleHeaderSetTypeId(tuphdr, erh->er_typeid);
+ HeapTupleHeaderSetTypMod(tuphdr, erh->er_typmod);
+ return;
+ }
+
+ /* Else allocation should match previous get_flat_size result */
+ Assert(allocated_size == erh->flat_size);
+
+ /* We'll need the tuple descriptor */
+ tupdesc = expanded_record_get_tupdesc(erh);
+
+ /* We must ensure that any pad space is zero-filled */
+ memset(tuphdr, 0, allocated_size);
+
+ /* Set up header fields of composite Datum */
+ HeapTupleHeaderSetDatumLength(tuphdr, allocated_size);
+ HeapTupleHeaderSetTypeId(tuphdr, erh->er_typeid);
+ HeapTupleHeaderSetTypMod(tuphdr, erh->er_typmod);
+ /* We also make sure that t_ctid is invalid unless explicitly set */
+ ItemPointerSetInvalid(&(tuphdr->t_ctid));
+
+ HeapTupleHeaderSetNatts(tuphdr, tupdesc->natts);
+ tuphdr->t_hoff = erh->hoff;
+
+ if (tupdesc->tdhasoid) /* else leave infomask = 0 */
+ tuphdr->t_infomask = HEAP_HASOID;
+
+ /* And fill the data area from dvalues/dnulls */
+ heap_fill_tuple(tupdesc,
+ erh->dvalues,
+ erh->dnulls,
+ (char *) tuphdr + erh->hoff,
+ erh->data_len,
+ &tuphdr->t_infomask,
+ (erh->hasnull ? tuphdr->t_bits : NULL));
+}
+
+/*
+ * Look up the tupdesc for the expanded record's actual type
+ *
+ * Note: code internal to this module is allowed to just fetch
+ * erh->er_tupdesc if ER_FLAG_DVALUES_VALID is set; otherwise it should call
+ * expanded_record_get_tupdesc. This function is the out-of-line portion
+ * of expanded_record_get_tupdesc.
+ */
+TupleDesc
+expanded_record_fetch_tupdesc(ExpandedRecordHeader *erh)
+{
+ TupleDesc tupdesc;
+
+ /* Easy if we already have it (but caller should have checked already) */
+ if (erh->er_tupdesc)
+ return erh->er_tupdesc;
+
+ /* Lookup the composite type's tupdesc using the typcache */
+ tupdesc = lookup_rowtype_tupdesc(erh->er_typeid, erh->er_typmod);
+
+ /*
+ * If it's a refcounted tupdesc rather than a statically allocated one, we
+ * want to manage the refcount with a memory context callback rather than
+ * assuming that the CurrentResourceOwner is longer-lived than this
+ * expanded object.
+ */
+ if (tupdesc->tdrefcount >= 0)
+ {
+ /* Register callback if we didn't already */
+ if (erh->er_mcb.arg == NULL)
+ {
+ erh->er_mcb.func = ER_mc_callback;
+ erh->er_mcb.arg = (void *) erh;
+ MemoryContextRegisterResetCallback(erh->hdr.eoh_context,
+ &erh->er_mcb);
+ }
+
+ /* Remember our own pointer */
+ erh->er_tupdesc = tupdesc;
+ tupdesc->tdrefcount++;
+
+ /* Release the pin lookup_rowtype_tupdesc acquired */
+ DecrTupleDescRefCount(tupdesc);
+ }
+ else
+ {
+ /* Just remember the pointer */
+ erh->er_tupdesc = tupdesc;
+ }
+
+ /* In either case, fetch the process-global ID for this tupdesc */
+ erh->er_tupdesc_id = assign_record_type_identifier(tupdesc->tdtypeid,
+ tupdesc->tdtypmod);
+
+ return tupdesc;
+}
+
+/*
+ * Get a HeapTuple representing the current value of the expanded record
+ *
+ * If valid, the originally stored tuple is returned, so caller must not
+ * scribble on it. Otherwise, we return a HeapTuple created in the current
+ * memory context. In either case, no attempt has been made to inline
+ * out-of-line toasted values, so the tuple isn't usable as a composite
+ * datum.
+ *
+ * Returns NULL if expanded record is empty.
+ */
+HeapTuple
+expanded_record_get_tuple(ExpandedRecordHeader *erh)
+{
+ /* Easy case if we still have original tuple */
+ if (erh->flags & ER_FLAG_FVALUE_VALID)
+ return erh->fvalue;
+
+ /* Else just build a tuple from datums */
+ if (erh->flags & ER_FLAG_DVALUES_VALID)
+ return heap_form_tuple(erh->er_tupdesc, erh->dvalues, erh->dnulls);
+
+ /* Expanded record is empty */
+ return NULL;
+}
+
+/*
+ * Memory context reset callback for cleaning up external resources
+ */
+static void
+ER_mc_callback(void *arg)
+{
+ ExpandedRecordHeader *erh = (ExpandedRecordHeader *) arg;
+ TupleDesc tupdesc = erh->er_tupdesc;
+
+ /* Release our privately-managed tupdesc refcount, if any */
+ if (tupdesc)
+ {
+ erh->er_tupdesc = NULL; /* just for luck */
+ if (tupdesc->tdrefcount > 0)
+ {
+ if (--tupdesc->tdrefcount == 0)
+ FreeTupleDesc(tupdesc);
+ }
+ }
+}
+
+/*
+ * DatumGetExpandedRecord: get a writable expanded record from an input argument
+ *
+ * Caution: if the input is a read/write pointer, this returns the input
+ * argument; so callers must be sure that their changes are "safe", that is
+ * they cannot leave the record in a corrupt state.
+ */
+ExpandedRecordHeader *
+DatumGetExpandedRecord(Datum d)
+{
+ /* If it's a writable expanded record already, just return it */
+ if (VARATT_IS_EXTERNAL_EXPANDED_RW(DatumGetPointer(d)))
+ {
+ ExpandedRecordHeader *erh = (ExpandedRecordHeader *) DatumGetEOHP(d);
+
+ Assert(erh->er_magic == ER_MAGIC);
+ return erh;
+ }
+
+ /* Else expand the hard way */
+ d = make_expanded_record_from_datum(d, CurrentMemoryContext);
+ return (ExpandedRecordHeader *) DatumGetEOHP(d);
+}
+
+/*
+ * Create the Datum/isnull representation of an expanded record object
+ * if we didn't do so already. After calling this, it's OK to read the
+ * dvalues/dnulls arrays directly, rather than going through get_field.
+ *
+ * Note that if the object is currently empty ("null"), this will change
+ * it to represent a row of nulls.
+ */
+void
+deconstruct_expanded_record(ExpandedRecordHeader *erh)
+{
+ TupleDesc tupdesc;
+ Datum *dvalues;
+ bool *dnulls;
+ int nfields;
+
+ if (erh->flags & ER_FLAG_DVALUES_VALID)
+ return; /* already valid, nothing to do */
+
+ /* We'll need the tuple descriptor */
+ tupdesc = expanded_record_get_tupdesc(erh);
+
+ /*
+ * Allocate arrays in private context, if we don't have them already. We
+ * don't expect to see a change in nfields here, so while we cope if it
+ * happens, we don't bother avoiding a leak of the old arrays (which might
+ * not be separately palloc'd, anyway).
+ */
+ nfields = tupdesc->natts;
+ if (erh->dvalues == NULL || erh->nfields != nfields)
+ {
+ char *chunk;
+
+ /*
+ * To save a palloc cycle, we allocate both the Datum and isnull
+ * arrays in one palloc chunk.
+ */
+ chunk = MemoryContextAlloc(erh->hdr.eoh_context,
+ nfields * (sizeof(Datum) + sizeof(bool)));
+ dvalues = (Datum *) chunk;
+ dnulls = (bool *) (chunk + nfields * sizeof(Datum));
+ erh->dvalues = dvalues;
+ erh->dnulls = dnulls;
+ erh->nfields = nfields;
+ }
+ else
+ {
+ dvalues = erh->dvalues;
+ dnulls = erh->dnulls;
+ }
+
+ if (erh->flags & ER_FLAG_FVALUE_VALID)
+ {
+ /* Deconstruct tuple */
+ heap_deform_tuple(erh->fvalue, tupdesc, dvalues, dnulls);
+ }
+ else
+ {
+ /* If record was empty, instantiate it as a row of nulls */
+ memset(dvalues, 0, nfields * sizeof(Datum));
+ memset(dnulls, true, nfields * sizeof(bool));
+ }
+
+ /* Mark the dvalues as valid */
+ erh->flags |= ER_FLAG_DVALUES_VALID;
+}
+
+/*
+ * Look up a record field by name
+ *
+ * If there is a field named "fieldname", fill in the contents of finfo
+ * and return "true". Else return "false" without changing *finfo.
+ */
+bool
+expanded_record_lookup_field(ExpandedRecordHeader *erh, const char *fieldname,
+ ExpandedRecordFieldInfo *finfo)
+{
+ TupleDesc tupdesc;
+ int fno;
+ Form_pg_attribute attr;
+
+ tupdesc = expanded_record_get_tupdesc(erh);
+
+ /* First, check user-defined attributes */
+ for (fno = 0; fno < tupdesc->natts; fno++)
+ {
+ attr = TupleDescAttr(tupdesc, fno);
+ if (namestrcmp(&attr->attname, fieldname) == 0 &&
+ !attr->attisdropped)
+ {
+ finfo->fnumber = attr->attnum;
+ finfo->ftypeid = attr->atttypid;
+ finfo->ftypmod = attr->atttypmod;
+ finfo->fcollation = attr->attcollation;
+ return true;
+ }
+ }
+
+ /* How about system attributes? */
+ attr = SystemAttributeByName(fieldname, tupdesc->tdhasoid);
+ if (attr != NULL)
+ {
+ finfo->fnumber = attr->attnum;
+ finfo->ftypeid = attr->atttypid;
+ finfo->ftypmod = attr->atttypmod;
+ finfo->fcollation = attr->attcollation;
+ return true;
+ }
+
+ return false;
+}
+
+/*
+ * Fetch value of record field
+ *
+ * expanded_record_get_field is the frontend for this; it handles the
+ * easy inline-able cases.
+ */
+Datum
+expanded_record_fetch_field(ExpandedRecordHeader *erh, int fnumber,
+ bool *isnull)
+{
+ if (fnumber > 0)
+ {
+ /* Empty record has null fields */
+ if (ExpandedRecordIsEmpty(erh))
+ {
+ *isnull = true;
+ return (Datum) 0;
+ }
+ /* Make sure we have deconstructed form */
+ deconstruct_expanded_record(erh);
+ /* Out-of-range field number reads as null */
+ if (unlikely(fnumber > erh->nfields))
+ {
+ *isnull = true;
+ return (Datum) 0;
+ }
+ *isnull = erh->dnulls[fnumber - 1];
+ return erh->dvalues[fnumber - 1];
+ }
+ else
+ {
+ /* System columns read as null if we haven't got flat tuple */
+ if (erh->fvalue == NULL)
+ {
+ *isnull = true;
+ return (Datum) 0;
+ }
+ /* heap_getsysattr doesn't actually use tupdesc, so just pass null */
+ return heap_getsysattr(erh->fvalue, fnumber, NULL, isnull);
+ }
+}
+
+/*
+ * Set value of record field
+ *
+ * If the expanded record is of domain type, the assignment will be rejected
+ * (without changing the record's state) if the domain's constraints would
+ * be violated.
+ *
+ * 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 check_constraints)
+{
+ TupleDesc tupdesc;
+ Form_pg_attribute attr;
+ Datum *dvalues;
+ bool *dnulls;
+ char *oldValue;
+
+ /*
+ * Shouldn't ever be trying to assign new data to a dummy header, except
+ * in the case of an internal call for field inlining.
+ */
+ Assert(!(erh->flags & ER_FLAG_IS_DUMMY) || !check_constraints);
+
+ /* Before performing the assignment, see if result will satisfy domain */
+ if ((erh->flags & ER_FLAG_IS_DOMAIN) && check_constraints)
+ check_domain_for_new_field(erh, fnumber, newValue, isnull);
+
+ /* If we haven't yet deconstructed the tuple, do that */
+ if (!(erh->flags & ER_FLAG_DVALUES_VALID))
+ deconstruct_expanded_record(erh);
+
+ /* Tuple descriptor must be valid by now */
+ tupdesc = erh->er_tupdesc;
+ Assert(erh->nfields == tupdesc->natts);
+
+ /* Caller error if fnumber is system column or nonexistent column */
+ if (unlikely(fnumber <= 0 || fnumber > erh->nfields))
+ elog(ERROR, "cannot assign to field %d of expanded record", fnumber);
+
+ /*
+ * Copy new field value into record's context, if needed.
+ */
+ attr = TupleDescAttr(tupdesc, fnumber - 1);
+ if (!isnull && !attr->attbyval)
+ {
+ MemoryContext oldcxt;
+
+ oldcxt = MemoryContextSwitchTo(erh->hdr.eoh_context);
+ newValue = datumCopy(newValue, false, attr->attlen);
+ MemoryContextSwitchTo(oldcxt);
+
+ /* 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.
+ */
+ if (attr->attlen == -1 &&
+ VARATT_IS_EXTERNAL(DatumGetPointer(newValue)))
+ erh->flags |= ER_FLAG_HAVE_EXTERNAL;
+ }
+
+ /*
+ * We're ready to make irreversible changes.
+ */
+ dvalues = erh->dvalues;
+ dnulls = erh->dnulls;
+
+ /* Flattened value will no longer represent record accurately */
+ erh->flags &= ~ER_FLAG_FVALUE_VALID;
+ /* And we don't know the flattened size either */
+ erh->flat_size = 0;
+
+ /* Grab old field value for pfree'ing, if needed. */
+ if (!attr->attbyval && !dnulls[fnumber - 1])
+ oldValue = (char *) DatumGetPointer(dvalues[fnumber - 1]);
+ else
+ oldValue = NULL;
+
+ /* And finally we can insert the new field. */
+ dvalues[fnumber - 1] = newValue;
+ dnulls[fnumber - 1] = isnull;
+
+ /*
+ * Free old field if needed; this keeps repeated field replacements from
+ * bloating the record's storage. If the pfree somehow fails, it won't
+ * corrupt the record.
+ *
+ * If we're updating a dummy header, we can't risk pfree'ing the old
+ * value, because most likely the expanded record's main header still has
+ * a pointer to it. This won't result in any sustained memory leak, since
+ * whatever we just allocated here is in the short-lived domain check
+ * context.
+ */
+ if (oldValue && !(erh->flags & ER_FLAG_IS_DUMMY))
+ {
+ /* Don't try to pfree a part of the original flat record */
+ if (oldValue < erh->fstartptr || oldValue >= erh->fendptr)
+ pfree(oldValue);
+ }
+}
+
+/*
+ * Set all record field(s)
+ *
+ * Caller must ensure that the provided datums are of the right types
+ * to match the record's previously assigned rowtype.
+ *
+ * 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.
+ */
+void
+expanded_record_set_fields(ExpandedRecordHeader *erh,
+ const Datum *newValues, const bool *isnulls)
+{
+ TupleDesc tupdesc;
+ Datum *dvalues;
+ bool *dnulls;
+ int fnumber;
+ MemoryContext oldcxt;
+
+ /* Shouldn't ever be trying to assign new data to a dummy header */
+ Assert(!(erh->flags & ER_FLAG_IS_DUMMY));
+
+ /* If we haven't yet deconstructed the tuple, do that */
+ if (!(erh->flags & ER_FLAG_DVALUES_VALID))
+ deconstruct_expanded_record(erh);
+
+ /* Tuple descriptor must be valid by now */
+ tupdesc = erh->er_tupdesc;
+ Assert(erh->nfields == tupdesc->natts);
+
+ /* Flattened value will no longer represent record accurately */
+ erh->flags &= ~ER_FLAG_FVALUE_VALID;
+ /* And we don't know the flattened size either */
+ erh->flat_size = 0;
+
+ oldcxt = MemoryContextSwitchTo(erh->hdr.eoh_context);
+
+ dvalues = erh->dvalues;
+ dnulls = erh->dnulls;
+
+ for (fnumber = 0; fnumber < erh->nfields; fnumber++)
+ {
+ Form_pg_attribute attr = TupleDescAttr(tupdesc, fnumber);
+ Datum newValue;
+ bool isnull;
+
+ /* Ignore dropped columns */
+ if (attr->attisdropped)
+ continue;
+
+ newValue = newValues[fnumber];
+ isnull = isnulls[fnumber];
+
+ if (!attr->attbyval)
+ {
+ /*
+ * Copy new field value into record's context, if needed.
+ */
+ if (!isnull)
+ {
+ 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;
+ }
+
+ /*
+ * Free old field value, if any (not likely, since really we ought
+ * to be inserting into an empty record).
+ */
+ if (unlikely(!dnulls[fnumber]))
+ {
+ char *oldValue;
+
+ oldValue = (char *) DatumGetPointer(dvalues[fnumber]);
+ /* Don't try to pfree a part of the original flat record */
+ if (oldValue < erh->fstartptr || oldValue >= erh->fendptr)
+ pfree(oldValue);
+ }
+ }
+
+ /* And finally we can insert the new field. */
+ dvalues[fnumber] = newValue;
+ dnulls[fnumber] = isnull;
+ }
+
+ /*
+ * Because we don't guarantee atomicity of set_fields(), we can just leave
+ * checking of domain constraints to occur as the final step; if it throws
+ * an error, too bad.
+ */
+ 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));
+
+ domain_check(ExpandedRecordGetRODatum(erh), false,
+ erh->er_decltypeid,
+ &erh->er_domaininfo,
+ erh->hdr.eoh_context);
+ }
+
+ MemoryContextSwitchTo(oldcxt);
+}
+
+/*
+ * Construct (or reset) working memory context for domain checks.
+ *
+ * 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.)
+ */
+static MemoryContext
+get_domain_check_cxt(ExpandedRecordHeader *erh)
+{
+ if (erh->er_domain_check_cxt == NULL)
+ erh->er_domain_check_cxt =
+ AllocSetContextCreate(erh->hdr.eoh_context,
+ "expanded record domain checks",
+ ALLOCSET_SMALL_SIZES);
+ else
+ MemoryContextReset(erh->er_domain_check_cxt);
+ return erh->er_domain_check_cxt;
+}
+
+/*
+ * Construct "dummy header" for checking domain constraints.
+ *
+ * Since we don't want to modify the state of the expanded record until
+ * we've validated the constraints, our approach is to set up a dummy
+ * record header containing the new field value(s) and then pass that to
+ * domain_check. We retain the dummy header as part of the expanded
+ * record's state to save palloc cycles, but reinitialize (most of)
+ * its contents on each use.
+ */
+static void
+build_dummy_expanded_header(ExpandedRecordHeader *main_erh)
+{
+ ExpandedRecordHeader *erh;
+ TupleDesc tupdesc = expanded_record_get_tupdesc(main_erh);
+
+ /* Ensure we have a domain_check_cxt */
+ (void) get_domain_check_cxt(main_erh);
+
+ /*
+ * Allocate dummy header on first time through, or in the unlikely event
+ * that the number of fields changes (in which case we just leak the old
+ * one). Include space for its field values in the request.
+ */
+ erh = main_erh->er_dummy_header;
+ if (erh == NULL || erh->nfields != tupdesc->natts)
+ {
+ char *chunk;
+
+ erh = (ExpandedRecordHeader *)
+ MemoryContextAlloc(main_erh->hdr.eoh_context,
+ MAXALIGN(sizeof(ExpandedRecordHeader))
+ + tupdesc->natts * (sizeof(Datum) + sizeof(bool)));
+
+ /* Ensure all header fields are initialized to 0/null */
+ memset(erh, 0, sizeof(ExpandedRecordHeader));
+
+ /*
+ * We set up the dummy header with an indication that its memory
+ * context is the short-lived context. This is so that, if any
+ * detoasting of out-of-line values happens due to an attempt to
+ * extract a composite datum from the dummy header, the detoasted
+ * stuff will end up in the short-lived context and not cause a leak.
+ * This is cheating a bit on the expanded-object protocol; but since
+ * we never pass a R/W pointer to the dummy object to any other code,
+ * 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);
+ erh->er_magic = ER_MAGIC;
+
+ /* Set up dvalues/dnulls, with no valid contents as yet */
+ chunk = (char *) erh + MAXALIGN(sizeof(ExpandedRecordHeader));
+ erh->dvalues = (Datum *) chunk;
+ erh->dnulls = (bool *) (chunk + tupdesc->natts * sizeof(Datum));
+ erh->nfields = tupdesc->natts;
+
+ /*
+ * The fields we just set are assumed to remain constant through
+ * multiple uses of the dummy header to check domain constraints. All
+ * other dummy header fields should be explicitly reset below, to
+ * ensure there's not accidental effects of one check on the next one.
+ */
+
+ main_erh->er_dummy_header = erh;
+ }
+
+ /*
+ * If anything inquires about the dummy header's declared type, it should
+ * report the composite base type, not the domain type (since the VALUE in
+ * a domain check constraint is of the base type not the domain). Hence
+ * we do not transfer over the IS_DOMAIN flag, nor indeed any of the main
+ * header's flags, since the dummy header is empty of data at this point.
+ * But don't forget to mark header as dummy.
+ */
+ erh->flags = ER_FLAG_IS_DUMMY;
+
+ /* Copy composite-type identification info */
+ erh->er_decltypeid = erh->er_typeid = main_erh->er_typeid;
+ erh->er_typmod = main_erh->er_typmod;
+
+ /* Dummy header does not need its own tupdesc refcount */
+ erh->er_tupdesc = tupdesc;
+ erh->er_tupdesc_id = main_erh->er_tupdesc_id;
+
+ /*
+ * It's tempting to copy over whatever we know about the flat size, but
+ * there's no point since we're surely about to modify the dummy record's
+ * field(s). Instead just clear anything left over from a previous usage
+ * cycle.
+ */
+ erh->flat_size = 0;
+
+ /* Copy over fvalue if we have it, so that system columns are available */
+ erh->fvalue = main_erh->fvalue;
+ erh->fstartptr = main_erh->fstartptr;
+ erh->fendptr = main_erh->fendptr;
+}
+
+/*
+ * Precheck domain constraints for a set_field operation
+ */
+static pg_noinline void
+check_domain_for_new_field(ExpandedRecordHeader *erh, int fnumber,
+ Datum newValue, bool isnull)
+{
+ ExpandedRecordHeader *dummy_erh;
+ MemoryContext oldcxt;
+
+ /* Construct dummy header to contain proposed new field set */
+ build_dummy_expanded_header(erh);
+ dummy_erh = erh->er_dummy_header;
+
+ /*
+ * If record isn't empty, just deconstruct it (if needed) and copy over
+ * the existing field values. If it is empty, just fill fields with nulls
+ * manually --- don't call deconstruct_expanded_record prematurely.
+ */
+ if (!ExpandedRecordIsEmpty(erh))
+ {
+ deconstruct_expanded_record(erh);
+ memcpy(dummy_erh->dvalues, erh->dvalues,
+ dummy_erh->nfields * sizeof(Datum));
+ memcpy(dummy_erh->dnulls, erh->dnulls,
+ dummy_erh->nfields * sizeof(bool));
+ /* There might be some external values in there... */
+ dummy_erh->flags |= erh->flags & ER_FLAG_HAVE_EXTERNAL;
+ }
+ else
+ {
+ memset(dummy_erh->dvalues, 0, dummy_erh->nfields * sizeof(Datum));
+ memset(dummy_erh->dnulls, true, dummy_erh->nfields * sizeof(bool));
+ }
+
+ /* Either way, we now have valid dvalues */
+ dummy_erh->flags |= ER_FLAG_DVALUES_VALID;
+
+ /* Caller error if fnumber is system column or nonexistent column */
+ if (unlikely(fnumber <= 0 || fnumber > dummy_erh->nfields))
+ elog(ERROR, "cannot assign to field %d of expanded record", fnumber);
+
+ /* Insert proposed new value into dummy field array */
+ dummy_erh->dvalues[fnumber - 1] = newValue;
+ dummy_erh->dnulls[fnumber - 1] = isnull;
+
+ /*
+ * The proposed new value might be external, in which case we'd better set
+ * the flag for that in dummy_erh. (This matters in case something in the
+ * domain check expressions tries to extract a flat value from the dummy
+ * header.)
+ */
+ if (!isnull)
+ {
+ Form_pg_attribute attr = TupleDescAttr(erh->er_tupdesc, fnumber - 1);
+
+ if (!attr->attbyval && attr->attlen == -1 &&
+ VARATT_IS_EXTERNAL(DatumGetPointer(newValue)))
+ dummy_erh->flags |= ER_FLAG_HAVE_EXTERNAL;
+ }
+
+ /*
+ * 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);
+
+ /*
+ * And now we can apply the check. Note we use main header's domain cache
+ * space, so that caching carries across repeated uses.
+ */
+ domain_check(ExpandedRecordGetRODatum(dummy_erh), false,
+ erh->er_decltypeid,
+ &erh->er_domaininfo,
+ erh->hdr.eoh_context);
+
+ MemoryContextSwitchTo(oldcxt);
+
+ /* We might as well clean up cruft immediately. */
+ MemoryContextReset(erh->er_domain_check_cxt);
+}
+
+/*
+ * Precheck domain constraints for a set_tuple operation
+ */
+static pg_noinline void
+check_domain_for_new_tuple(ExpandedRecordHeader *erh, HeapTuple tuple)
+{
+ ExpandedRecordHeader *dummy_erh;
+ MemoryContext oldcxt;
+
+ /* If we're being told to set record to empty, just see if NULL is OK */
+ if (tuple == NULL)
+ {
+ /* We run domain_check in a short-lived context to limit cruft */
+ oldcxt = MemoryContextSwitchTo(get_domain_check_cxt(erh));
+
+ domain_check((Datum) 0, true,
+ erh->er_decltypeid,
+ &erh->er_domaininfo,
+ erh->hdr.eoh_context);
+
+ MemoryContextSwitchTo(oldcxt);
+
+ /* We might as well clean up cruft immediately. */
+ MemoryContextReset(erh->er_domain_check_cxt);
+
+ return;
+ }
+
+ /* Construct dummy header to contain replacement tuple */
+ build_dummy_expanded_header(erh);
+ dummy_erh = erh->er_dummy_header;
+
+ /* Insert tuple, but don't bother to deconstruct its fields for now */
+ dummy_erh->fvalue = tuple;
+ dummy_erh->fstartptr = (char *) tuple->t_data;
+ dummy_erh->fendptr = ((char *) tuple->t_data) + tuple->t_len;
+ dummy_erh->flags |= ER_FLAG_FVALUE_VALID;
+
+ /* Remember if we have any out-of-line field values */
+ if (HeapTupleHasExternal(tuple))
+ dummy_erh->flags |= ER_FLAG_HAVE_EXTERNAL;
+
+ /*
+ * 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);
+
+ /*
+ * And now we can apply the check. Note we use main header's domain cache
+ * space, so that caching carries across repeated uses.
+ */
+ domain_check(ExpandedRecordGetRODatum(dummy_erh), false,
+ erh->er_decltypeid,
+ &erh->er_domaininfo,
+ erh->hdr.eoh_context);
+
+ MemoryContextSwitchTo(oldcxt);
+
+ /* We might as well clean up cruft immediately. */
+ MemoryContextReset(erh->er_domain_check_cxt);
+}
LWTRANCHE_SESSION_TYPMOD_TABLE
};
+/* hashtable for recognizing registered record types */
static HTAB *RecordCacheHash = NULL;
+/* arrays of info about registered record types, indexed by assigned typmod */
static TupleDesc *RecordCacheArray = NULL;
-static int32 RecordCacheArrayLen = 0; /* allocated length of array */
+static uint64 *RecordIdentifierArray = NULL;
+static int32 RecordCacheArrayLen = 0; /* allocated length of above arrays */
static int32 NextRecordTypmod = 0; /* number of entries used */
+/*
+ * Process-wide counter for generating unique tupledesc identifiers.
+ * Zero and one (INVALID_TUPLEDESC_IDENTIFIER) aren't allowed to be chosen
+ * as identifiers, so we start the counter at INVALID_TUPLEDESC_IDENTIFIER.
+ */
+static uint64 tupledesc_id_counter = INVALID_TUPLEDESC_IDENTIFIER;
+
static void load_typcache_tupdesc(TypeCacheEntry *typentry);
static void load_rangetype_info(TypeCacheEntry *typentry);
static void load_domaintype_info(TypeCacheEntry *typentry);
typentry->tupDesc->tdrefcount++;
/*
- * In future, we could take some pains to not increment the seqno if the
- * tupdesc didn't really change; but for now it's not worth it.
+ * In future, we could take some pains to not change tupDesc_identifier if
+ * the tupdesc didn't really change; but for now it's not worth it.
*/
- typentry->tupDescSeqNo++;
+ typentry->tupDesc_identifier = ++tupledesc_id_counter;
relation_close(rel, AccessShareLock);
}
}
/*
- * Make sure that RecordCacheArray is large enough to store 'typmod'.
+ * Make sure that RecordCacheArray and RecordIdentifierArray are large enough
+ * to store 'typmod'.
*/
static void
ensure_record_cache_typmod_slot_exists(int32 typmod)
{
RecordCacheArray = (TupleDesc *)
MemoryContextAllocZero(CacheMemoryContext, 64 * sizeof(TupleDesc));
+ RecordIdentifierArray = (uint64 *)
+ MemoryContextAllocZero(CacheMemoryContext, 64 * sizeof(uint64));
RecordCacheArrayLen = 64;
}
newlen * sizeof(TupleDesc));
memset(RecordCacheArray + RecordCacheArrayLen, 0,
(newlen - RecordCacheArrayLen) * sizeof(TupleDesc));
+ RecordIdentifierArray = (uint64 *) repalloc(RecordIdentifierArray,
+ newlen * sizeof(uint64));
+ memset(RecordIdentifierArray + RecordCacheArrayLen, 0,
+ (newlen - RecordCacheArrayLen) * sizeof(uint64));
RecordCacheArrayLen = newlen;
}
}
/*
* Our local array can now point directly to the TupleDesc
- * in shared memory.
+ * in shared memory, which is non-reference-counted.
*/
RecordCacheArray[typmod] = tupdesc;
Assert(tupdesc->tdrefcount == -1);
+ /*
+ * We don't share tupdesc identifiers across processes, so
+ * assign one locally.
+ */
+ RecordIdentifierArray[typmod] = ++tupledesc_id_counter;
+
dshash_release_lock(CurrentSession->shared_typmod_table,
entry);
RecordCacheArray[entDesc->tdtypmod] = entDesc;
recentry->tupdesc = entDesc;
+ /* Assign a unique tupdesc identifier, too. */
+ RecordIdentifierArray[entDesc->tdtypmod] = ++tupledesc_id_counter;
+
/* Update the caller's tuple descriptor. */
tupDesc->tdtypmod = entDesc->tdtypmod;
MemoryContextSwitchTo(oldcxt);
}
+/*
+ * assign_record_type_identifier
+ *
+ * Get an identifier, which will be unique over the lifespan of this backend
+ * process, for the current tuple descriptor of the specified composite type.
+ * For named composite types, the value is guaranteed to change if the type's
+ * definition does. For registered RECORD types, the value will not change
+ * once assigned, since the registered type won't either. If an anonymous
+ * RECORD type is specified, we return a new identifier on each call.
+ */
+uint64
+assign_record_type_identifier(Oid type_id, int32 typmod)
+{
+ if (type_id != RECORDOID)
+ {
+ /*
+ * It's a named composite type, so use the regular typcache.
+ */
+ TypeCacheEntry *typentry;
+
+ typentry = lookup_type_cache(type_id, TYPECACHE_TUPDESC);
+ if (typentry->tupDesc == NULL)
+ ereport(ERROR,
+ (errcode(ERRCODE_WRONG_OBJECT_TYPE),
+ errmsg("type %s is not composite",
+ format_type_be(type_id))));
+ Assert(typentry->tupDesc_identifier != 0);
+ return typentry->tupDesc_identifier;
+ }
+ else
+ {
+ /*
+ * It's a transient record type, so look in our record-type table.
+ */
+ if (typmod >= 0 && typmod < RecordCacheArrayLen &&
+ RecordCacheArray[typmod] != NULL)
+ {
+ Assert(RecordIdentifierArray[typmod] != 0);
+ return RecordIdentifierArray[typmod];
+ }
+
+ /* For anonymous or unrecognized record type, generate a new ID */
+ return ++tupledesc_id_counter;
+ }
+}
+
/*
* Return the amout of shmem required to hold a SharedRecordTypmodRegistry.
* This exists only to avoid exposing private innards of
--- /dev/null
+/*-------------------------------------------------------------------------
+ *
+ * expandedrecord.h
+ * Declarations for composite expanded objects.
+ *
+ * Portions Copyright (c) 1996-2018, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/utils/expandedrecord.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef EXPANDEDRECORD_H
+#define EXPANDEDRECORD_H
+
+#include "access/htup.h"
+#include "access/tupdesc.h"
+#include "fmgr.h"
+#include "utils/expandeddatum.h"
+
+
+/*
+ * An expanded record is contained within a private memory context (as
+ * all expanded objects must be) and has a control structure as below.
+ *
+ * The expanded record might contain a regular "flat" tuple if that was the
+ * original input and we've not modified it. Otherwise, the contents are
+ * represented by Datum/isnull arrays plus type information. We could also
+ * have both forms, if we've deconstructed the original tuple for access
+ * purposes but not yet changed it. For pass-by-reference field types, the
+ * Datums would point into the flat tuple in this situation. Once we start
+ * modifying tuple fields, new pass-by-ref fields are separately palloc'd
+ * within the memory context.
+ *
+ * It's possible to build an expanded record that references a "flat" tuple
+ * stored externally, if the caller can guarantee that that tuple will not
+ * change for the lifetime of the expanded record. (This frammish is mainly
+ * meant to avoid unnecessary data copying in trigger functions.)
+ */
+#define ER_MAGIC 1384727874 /* ID for debugging crosschecks */
+
+typedef struct ExpandedRecordHeader
+{
+ /* Standard header for expanded objects */
+ ExpandedObjectHeader hdr;
+
+ /* Magic value identifying an expanded record (for debugging only) */
+ int er_magic;
+
+ /* Assorted flag bits */
+ int flags;
+#define ER_FLAG_FVALUE_VALID 0x0001 /* fvalue is up to date? */
+#define ER_FLAG_FVALUE_ALLOCED 0x0002 /* fvalue is local storage? */
+#define ER_FLAG_DVALUES_VALID 0x0004 /* dvalues/dnulls are up to date? */
+#define ER_FLAG_DVALUES_ALLOCED 0x0008 /* any field values local storage? */
+#define ER_FLAG_HAVE_EXTERNAL 0x0010 /* any field values are external? */
+#define ER_FLAG_TUPDESC_ALLOCED 0x0020 /* tupdesc is local storage? */
+#define ER_FLAG_IS_DOMAIN 0x0040 /* er_decltypeid is domain? */
+#define ER_FLAG_IS_DUMMY 0x0080 /* this header is dummy (see below) */
+/* flag bits that are not to be cleared when replacing tuple data: */
+#define ER_FLAGS_NON_DATA \
+ (ER_FLAG_TUPDESC_ALLOCED | ER_FLAG_IS_DOMAIN | ER_FLAG_IS_DUMMY)
+
+ /* Declared type of the record variable (could be a domain type) */
+ Oid er_decltypeid;
+
+ /*
+ * Actual composite type/typmod; never a domain (if ER_FLAG_IS_DOMAIN,
+ * these identify the composite base type). These will match
+ * er_tupdesc->tdtypeid/tdtypmod, as well as the header fields of
+ * composite datums made from or stored in this expanded record.
+ */
+ Oid er_typeid; /* type OID of the composite type */
+ int32 er_typmod; /* typmod of the composite type */
+
+ /*
+ * Tuple descriptor, if we have one, else NULL. This may point to a
+ * reference-counted tupdesc originally belonging to the typcache, in
+ * which case we use a memory context reset callback to release the
+ * refcount. It can also be locally allocated in this object's private
+ * context (in which case ER_FLAG_TUPDESC_ALLOCED is set).
+ */
+ TupleDesc er_tupdesc;
+
+ /*
+ * Unique-within-process identifier for the tupdesc (see typcache.h). This
+ * field will never be equal to INVALID_TUPLEDESC_IDENTIFIER.
+ */
+ uint64 er_tupdesc_id;
+
+ /*
+ * If we have a Datum-array representation of the record, it's kept here;
+ * else ER_FLAG_DVALUES_VALID is not set, and dvalues/dnulls may be NULL
+ * if they've not yet been allocated. If allocated, the dvalues and
+ * dnulls arrays are palloc'd within the object private context, and are
+ * of length matching er_tupdesc->natts. For pass-by-ref field types,
+ * dvalues entries might point either into the fstartptr..fendptr area, or
+ * to separately palloc'd chunks.
+ */
+ Datum *dvalues; /* array of Datums */
+ bool *dnulls; /* array of is-null flags for Datums */
+ int nfields; /* length of above arrays */
+
+ /*
+ * flat_size is the current space requirement for the flat equivalent of
+ * the expanded record, if known; otherwise it's 0. We store this to make
+ * consecutive calls of get_flat_size cheap. If flat_size is not 0, the
+ * component values data_len, hoff, and hasnull must be valid too.
+ */
+ Size flat_size;
+
+ Size data_len; /* data len within flat_size */
+ int hoff; /* header offset */
+ bool hasnull; /* null bitmap needed? */
+
+ /*
+ * fvalue points to the flat representation if we have one, else it is
+ * NULL. If the flat representation is valid (up to date) then
+ * ER_FLAG_FVALUE_VALID is set. Even if we've outdated the flat
+ * representation due to changes of user fields, it can still be used to
+ * fetch system column values. If we have a flat representation then
+ * fstartptr/fendptr point to the start and end+1 of its data area; this
+ * is so that we can tell which Datum pointers point into the flat
+ * representation rather than being pointers to separately palloc'd data.
+ */
+ HeapTuple fvalue; /* might or might not be private storage */
+ char *fstartptr; /* start of its data area */
+ char *fendptr; /* end+1 of its data area */
+
+ /* 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() */
+
+ /* Callback info (it's active if er_mcb.arg is not NULL) */
+ MemoryContextCallback er_mcb;
+} ExpandedRecordHeader;
+
+/* fmgr macros for expanded record objects */
+#define PG_GETARG_EXPANDED_RECORD(n) DatumGetExpandedRecord(PG_GETARG_DATUM(n))
+#define ExpandedRecordGetDatum(erh) EOHPGetRWDatum(&(erh)->hdr)
+#define ExpandedRecordGetRODatum(erh) EOHPGetRODatum(&(erh)->hdr)
+#define PG_RETURN_EXPANDED_RECORD(x) PG_RETURN_DATUM(ExpandedRecordGetDatum(x))
+
+/* assorted other macros */
+#define ExpandedRecordIsEmpty(erh) \
+ (((erh)->flags & (ER_FLAG_DVALUES_VALID | ER_FLAG_FVALUE_VALID)) == 0)
+#define ExpandedRecordIsDomain(erh) \
+ (((erh)->flags & ER_FLAG_IS_DOMAIN) != 0)
+
+/* this can substitute for TransferExpandedObject() when we already have erh */
+#define TransferExpandedRecord(erh, cxt) \
+ MemoryContextSetParent((erh)->hdr.eoh_context, cxt)
+
+/* information returned by expanded_record_lookup_field() */
+typedef struct ExpandedRecordFieldInfo
+{
+ int fnumber; /* field's attr number in record */
+ Oid ftypeid; /* field's type/typmod info */
+ int32 ftypmod;
+ Oid fcollation; /* field's collation if any */
+} ExpandedRecordFieldInfo;
+
+/*
+ * prototypes for functions defined in expandedrecord.c
+ */
+extern ExpandedRecordHeader *make_expanded_record_from_typeid(Oid type_id, int32 typmod,
+ MemoryContext parentcontext);
+extern ExpandedRecordHeader *make_expanded_record_from_tupdesc(TupleDesc tupdesc,
+ MemoryContext parentcontext);
+extern ExpandedRecordHeader *make_expanded_record_from_exprecord(ExpandedRecordHeader *olderh,
+ MemoryContext parentcontext);
+extern void expanded_record_set_tuple(ExpandedRecordHeader *erh,
+ HeapTuple tuple, bool copy);
+extern Datum make_expanded_record_from_datum(Datum recorddatum,
+ MemoryContext parentcontext);
+extern TupleDesc expanded_record_fetch_tupdesc(ExpandedRecordHeader *erh);
+extern HeapTuple expanded_record_get_tuple(ExpandedRecordHeader *erh);
+extern ExpandedRecordHeader *DatumGetExpandedRecord(Datum d);
+extern void deconstruct_expanded_record(ExpandedRecordHeader *erh);
+extern bool expanded_record_lookup_field(ExpandedRecordHeader *erh,
+ const char *fieldname,
+ ExpandedRecordFieldInfo *finfo);
+extern Datum expanded_record_fetch_field(ExpandedRecordHeader *erh, int fnumber,
+ bool *isnull);
+extern void expanded_record_set_field_internal(ExpandedRecordHeader *erh,
+ int fnumber,
+ Datum newValue, bool isnull,
+ bool check_constraints);
+extern void expanded_record_set_fields(ExpandedRecordHeader *erh,
+ const Datum *newValues, const bool *isnulls);
+
+/* 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)
+
+/*
+ * Inline-able fast cases. The expanded_record_fetch_xxx functions above
+ * handle the general cases.
+ */
+
+/* Get the tupdesc for the expanded record's actual type */
+static inline TupleDesc
+expanded_record_get_tupdesc(ExpandedRecordHeader *erh)
+{
+ if (likely(erh->er_tupdesc != NULL))
+ return erh->er_tupdesc;
+ else
+ return expanded_record_fetch_tupdesc(erh);
+}
+
+/* Get value of record field */
+static inline Datum
+expanded_record_get_field(ExpandedRecordHeader *erh, int fnumber,
+ bool *isnull)
+{
+ if ((erh->flags & ER_FLAG_DVALUES_VALID) &&
+ likely(fnumber > 0 && fnumber <= erh->nfields))
+ {
+ *isnull = erh->dnulls[fnumber - 1];
+ return erh->dvalues[fnumber - 1];
+ }
+ else
+ return expanded_record_fetch_field(erh, fnumber, isnull);
+}
+
+#endif /* EXPANDEDRECORD_H */
/*
* Tuple descriptor if it's a composite type (row type). NULL if not
* composite or information hasn't yet been requested. (NOTE: this is a
- * reference-counted tupledesc.) To simplify caching dependent info,
- * tupDescSeqNo is incremented each time tupDesc is rebuilt in a session.
+ * reference-counted tupledesc.)
+ *
+ * To simplify caching dependent info, tupDesc_identifier is an identifier
+ * for this tupledesc that is unique for the life of the process, and
+ * changes anytime the tupledesc does. Zero if not yet determined.
*/
TupleDesc tupDesc;
- int64 tupDescSeqNo;
+ uint64 tupDesc_identifier;
/*
* Fields computed when TYPECACHE_RANGE_INFO is requested. Zeroes if not
#define TYPECACHE_HASH_EXTENDED_PROC 0x4000
#define TYPECACHE_HASH_EXTENDED_PROC_FINFO 0x8000
+/* This value will not equal any valid tupledesc identifier, nor 0 */
+#define INVALID_TUPLEDESC_IDENTIFIER ((uint64) 1)
+
/*
* Callers wishing to maintain a long-lived reference to a domain's constraint
* set must store it in one of these. Use InitDomainConstraintRef() and
extern void assign_record_type_typmod(TupleDesc tupDesc);
+extern uint64 assign_record_type_identifier(Oid type_id, int32 typmod);
+
extern int compare_values_of_enum(TypeCacheEntry *tcache, Oid arg1, Oid arg2);
extern size_t SharedRecordTypmodRegistryEstimate(void);
REGRESS_OPTS = --dbname=$(PL_TESTDB)
-REGRESS = plpgsql_call plpgsql_control plpgsql_transaction
+REGRESS = plpgsql_call plpgsql_control plpgsql_record plpgsql_transaction
all: all-lib
--- /dev/null
+--
+-- Tests for PL/pgSQL handling of composite (record) variables
+--
+create type two_int4s as (f1 int4, f2 int4);
+create type two_int8s as (q1 int8, q2 int8);
+-- base-case return of a composite type
+create function retc(int) returns two_int8s language plpgsql as
+$$ begin return row($1,1)::two_int8s; end $$;
+select retc(42);
+ retc
+--------
+ (42,1)
+(1 row)
+
+-- ok to return a matching record type
+create or replace function retc(int) returns two_int8s language plpgsql as
+$$ begin return row($1::int8, 1::int8); end $$;
+select retc(42);
+ retc
+--------
+ (42,1)
+(1 row)
+
+-- we don't currently support implicit casting
+create or replace function retc(int) returns two_int8s language plpgsql as
+$$ begin return row($1,1); end $$;
+select retc(42);
+ERROR: returned record type does not match expected record type
+DETAIL: Returned type integer does not match expected type bigint in column 1.
+CONTEXT: PL/pgSQL function retc(integer) while casting return value to function's return type
+-- nor extra columns
+create or replace function retc(int) returns two_int8s language plpgsql as
+$$ begin return row($1::int8, 1::int8, 42); end $$;
+select retc(42);
+ERROR: returned record type does not match expected record type
+DETAIL: Number of returned columns (3) does not match expected column count (2).
+CONTEXT: PL/pgSQL function retc(integer) while casting return value to function's return type
+-- same cases with an intermediate "record" variable
+create or replace function retc(int) returns two_int8s language plpgsql as
+$$ declare r record; begin r := row($1::int8, 1::int8); return r; end $$;
+select retc(42);
+ retc
+--------
+ (42,1)
+(1 row)
+
+create or replace function retc(int) returns two_int8s language plpgsql as
+$$ declare r record; begin r := row($1,1); return r; end $$;
+select retc(42);
+ERROR: returned record type does not match expected record type
+DETAIL: Returned type integer does not match expected type bigint in column 1.
+CONTEXT: PL/pgSQL function retc(integer) while casting return value to function's return type
+create or replace function retc(int) returns two_int8s language plpgsql as
+$$ declare r record; begin r := row($1::int8, 1::int8, 42); return r; end $$;
+select retc(42);
+ERROR: returned record type does not match expected record type
+DETAIL: Number of returned columns (3) does not match expected column count (2).
+CONTEXT: PL/pgSQL function retc(integer) while casting return value to function's return type
+-- but, for mostly historical reasons, we do convert when assigning
+-- to a named-composite-type variable
+create or replace function retc(int) returns two_int8s language plpgsql as
+$$ declare r two_int8s; begin r := row($1::int8, 1::int8, 42); return r; end $$;
+select retc(42);
+ retc
+--------
+ (42,1)
+(1 row)
+
+do $$ declare c two_int8s;
+begin c := row(1,2); raise notice 'c = %', c; end$$;
+NOTICE: c = (1,2)
+do $$ declare c two_int8s;
+begin for c in select 1,2 loop raise notice 'c = %', c; end loop; end$$;
+NOTICE: c = (1,2)
+do $$ declare c4 two_int4s; c8 two_int8s;
+begin
+ c8 := row(1,2);
+ c4 := c8;
+ c8 := c4;
+ raise notice 'c4 = %', c4;
+ raise notice 'c8 = %', c8;
+end$$;
+NOTICE: c4 = (1,2)
+NOTICE: c8 = (1,2)
+-- check passing composite result to another function
+create function getq1(two_int8s) returns int8 language plpgsql as $$
+declare r two_int8s; begin r := $1; return r.q1; end $$;
+select getq1(retc(344));
+ getq1
+-------
+ 344
+(1 row)
+
+select getq1(row(1,2));
+ getq1
+-------
+ 1
+(1 row)
+
+do $$
+declare r1 two_int8s; r2 record; x int8;
+begin
+ r1 := retc(345);
+ perform getq1(r1);
+ x := getq1(r1);
+ raise notice 'x = %', x;
+ r2 := retc(346);
+ perform getq1(r2);
+ x := getq1(r2);
+ raise notice 'x = %', x;
+end$$;
+NOTICE: x = 345
+NOTICE: x = 346
+-- check assignments of composites
+do $$
+declare r1 two_int8s; r2 two_int8s; r3 record; r4 record;
+begin
+ r1 := row(1,2);
+ raise notice 'r1 = %', r1;
+ r1 := r1; -- shouldn't do anything
+ raise notice 'r1 = %', r1;
+ r2 := r1;
+ raise notice 'r1 = %', r1;
+ raise notice 'r2 = %', r2;
+ r2.q2 = r1.q1 + 3; -- check that r2 has distinct storage
+ raise notice 'r1 = %', r1;
+ raise notice 'r2 = %', r2;
+ r1 := null;
+ raise notice 'r1 = %', r1;
+ raise notice 'r2 = %', r2;
+ r1 := row(7,11)::two_int8s;
+ r2 := r1;
+ raise notice 'r1 = %', r1;
+ raise notice 'r2 = %', r2;
+ r3 := row(1,2);
+ r4 := r3;
+ raise notice 'r3 = %', r3;
+ raise notice 'r4 = %', r4;
+ r4.f1 := r4.f1 + 3; -- check that r4 has distinct storage
+ raise notice 'r3 = %', r3;
+ raise notice 'r4 = %', r4;
+ r1 := r3;
+ raise notice 'r1 = %', r1;
+ r4 := r1;
+ raise notice 'r4 = %', r4;
+ r4.q2 := r4.q2 + 1; -- r4's field names have changed
+ raise notice 'r4 = %', r4;
+end$$;
+NOTICE: r1 = (1,2)
+NOTICE: r1 = (1,2)
+NOTICE: r1 = (1,2)
+NOTICE: r2 = (1,2)
+NOTICE: r1 = (1,2)
+NOTICE: r2 = (1,4)
+NOTICE: r1 = <NULL>
+NOTICE: r2 = (1,4)
+NOTICE: r1 = (7,11)
+NOTICE: r2 = (7,11)
+NOTICE: r3 = (1,2)
+NOTICE: r4 = (1,2)
+NOTICE: r3 = (1,2)
+NOTICE: r4 = (4,2)
+NOTICE: r1 = (1,2)
+NOTICE: r4 = (1,2)
+NOTICE: r4 = (1,3)
+-- fields of named-type vars read as null if uninitialized
+do $$
+declare r1 two_int8s;
+begin
+ raise notice 'r1 = %', r1;
+ raise notice 'r1.q1 = %', r1.q1;
+ raise notice 'r1.q2 = %', r1.q2;
+ raise notice 'r1 = %', r1;
+end$$;
+NOTICE: r1 = <NULL>
+NOTICE: r1.q1 = <NULL>
+NOTICE: r1.q2 = <NULL>
+NOTICE: r1 = <NULL>
+do $$
+declare r1 two_int8s;
+begin
+ raise notice 'r1.q1 = %', r1.q1;
+ raise notice 'r1.q2 = %', r1.q2;
+ raise notice 'r1 = %', r1;
+ raise notice 'r1.nosuchfield = %', r1.nosuchfield;
+end$$;
+NOTICE: r1.q1 = <NULL>
+NOTICE: r1.q2 = <NULL>
+NOTICE: r1 = <NULL>
+ERROR: record "r1" has no field "nosuchfield"
+CONTEXT: SQL statement "SELECT r1.nosuchfield"
+PL/pgSQL function inline_code_block line 7 at RAISE
+-- records, not so much
+do $$
+declare r1 record;
+begin
+ raise notice 'r1 = %', r1;
+ raise notice 'r1.f1 = %', r1.f1;
+ raise notice 'r1.f2 = %', r1.f2;
+ raise notice 'r1 = %', r1;
+end$$;
+NOTICE: r1 = <NULL>
+ERROR: record "r1" is not assigned yet
+DETAIL: The tuple structure of a not-yet-assigned record is indeterminate.
+CONTEXT: SQL statement "SELECT r1.f1"
+PL/pgSQL function inline_code_block line 5 at RAISE
+-- but OK if you assign first
+do $$
+declare r1 record;
+begin
+ raise notice 'r1 = %', r1;
+ r1 := row(1,2);
+ raise notice 'r1.f1 = %', r1.f1;
+ raise notice 'r1.f2 = %', r1.f2;
+ raise notice 'r1 = %', r1;
+ raise notice 'r1.nosuchfield = %', r1.nosuchfield;
+end$$;
+NOTICE: r1 = <NULL>
+NOTICE: r1.f1 = 1
+NOTICE: r1.f2 = 2
+NOTICE: r1 = (1,2)
+ERROR: record "r1" has no field "nosuchfield"
+CONTEXT: SQL statement "SELECT r1.nosuchfield"
+PL/pgSQL function inline_code_block line 9 at RAISE
+-- check repeated assignments to composite fields
+create table some_table (id int, data text);
+do $$
+declare r some_table;
+begin
+ r := (23, 'skidoo');
+ for i in 1 .. 10 loop
+ r.id := r.id + i;
+ r.data := r.data || ' ' || i;
+ end loop;
+ raise notice 'r = %', r;
+end$$;
+NOTICE: r = (78,"skidoo 1 2 3 4 5 6 7 8 9 10")
+-- check behavior of function declared to return "record"
+create function returnsrecord(int) returns record language plpgsql as
+$$ begin return row($1,$1+1); end $$;
+select returnsrecord(42);
+ returnsrecord
+---------------
+ (42,43)
+(1 row)
+
+select * from returnsrecord(42) as r(x int, y int);
+ x | y
+----+----
+ 42 | 43
+(1 row)
+
+select * from returnsrecord(42) as r(x int, y int, z int); -- fail
+ERROR: returned record type does not match expected record type
+DETAIL: Number of returned columns (2) does not match expected column count (3).
+CONTEXT: PL/pgSQL function returnsrecord(integer) while casting return value to function's return type
+select * from returnsrecord(42) as r(x int, y bigint); -- fail
+ERROR: returned record type does not match expected record type
+DETAIL: Returned type integer does not match expected type bigint in column 2.
+CONTEXT: PL/pgSQL function returnsrecord(integer) while casting return value to function's return type
+-- same with an intermediate record variable
+create or replace function returnsrecord(int) returns record language plpgsql as
+$$ declare r record; begin r := row($1,$1+1); return r; end $$;
+select returnsrecord(42);
+ returnsrecord
+---------------
+ (42,43)
+(1 row)
+
+select * from returnsrecord(42) as r(x int, y int);
+ x | y
+----+----
+ 42 | 43
+(1 row)
+
+select * from returnsrecord(42) as r(x int, y int, z int); -- fail
+ERROR: returned record type does not match expected record type
+DETAIL: Number of returned columns (2) does not match expected column count (3).
+CONTEXT: PL/pgSQL function returnsrecord(integer) while casting return value to function's return type
+select * from returnsrecord(42) as r(x int, y bigint); -- fail
+ERROR: returned record type does not match expected record type
+DETAIL: Returned type integer does not match expected type bigint in column 2.
+CONTEXT: PL/pgSQL function returnsrecord(integer) while casting return value to function's return type
+-- should work the same with a missing column in the actual result value
+create table has_hole(f1 int, f2 int, f3 int);
+alter table has_hole drop column f2;
+create or replace function returnsrecord(int) returns record language plpgsql as
+$$ begin return row($1,$1+1)::has_hole; end $$;
+select returnsrecord(42);
+ returnsrecord
+---------------
+ (42,43)
+(1 row)
+
+select * from returnsrecord(42) as r(x int, y int);
+ x | y
+----+----
+ 42 | 43
+(1 row)
+
+select * from returnsrecord(42) as r(x int, y int, z int); -- fail
+ERROR: returned record type does not match expected record type
+DETAIL: Number of returned columns (2) does not match expected column count (3).
+CONTEXT: PL/pgSQL function returnsrecord(integer) while casting return value to function's return type
+select * from returnsrecord(42) as r(x int, y bigint); -- fail
+ERROR: returned record type does not match expected record type
+DETAIL: Returned type integer does not match expected type bigint in column 2.
+CONTEXT: PL/pgSQL function returnsrecord(integer) while casting return value to function's return type
+-- same with an intermediate record variable
+create or replace function returnsrecord(int) returns record language plpgsql as
+$$ declare r record; begin r := row($1,$1+1)::has_hole; return r; end $$;
+select returnsrecord(42);
+ returnsrecord
+---------------
+ (42,43)
+(1 row)
+
+select * from returnsrecord(42) as r(x int, y int);
+ x | y
+----+----
+ 42 | 43
+(1 row)
+
+select * from returnsrecord(42) as r(x int, y int, z int); -- fail
+ERROR: returned record type does not match expected record type
+DETAIL: Number of returned columns (2) does not match expected column count (3).
+CONTEXT: PL/pgSQL function returnsrecord(integer) while casting return value to function's return type
+select * from returnsrecord(42) as r(x int, y bigint); -- fail
+ERROR: returned record type does not match expected record type
+DETAIL: Returned type integer does not match expected type bigint in column 2.
+CONTEXT: PL/pgSQL function returnsrecord(integer) while casting return value to function's return type
+-- check access to a field of an argument declared "record"
+create function getf1(x record) returns int language plpgsql as
+$$ begin return x.f1; end $$;
+select getf1(1);
+ERROR: function getf1(integer) does not exist
+LINE 1: select getf1(1);
+ ^
+HINT: No function matches the given name and argument types. You might need to add explicit type casts.
+select getf1(row(1,2));
+ getf1
+-------
+ 1
+(1 row)
+
+select getf1(row(1,2)::two_int8s);
+ERROR: record "x" has no field "f1"
+CONTEXT: PL/pgSQL function getf1(record) line 1 at RETURN
+select getf1(row(1,2));
+ getf1
+-------
+ 1
+(1 row)
+
+-- check behavior when assignment to FOR-loop variable requires coercion
+do $$
+declare r two_int8s;
+begin
+ for r in select i, i+1 from generate_series(1,4) i
+ loop
+ raise notice 'r = %', r;
+ end loop;
+end$$;
+NOTICE: r = (1,2)
+NOTICE: r = (2,3)
+NOTICE: r = (3,4)
+NOTICE: r = (4,5)
+-- check behavior when returning setof composite
+create function returnssetofholes() returns setof has_hole language plpgsql as
+$$
+declare r record;
+ h has_hole;
+begin
+ return next h;
+ r := (1,2);
+ h := (3,4);
+ return next r;
+ return next h;
+ return next row(5,6);
+ return next row(7,8)::has_hole;
+end$$;
+select returnssetofholes();
+ returnssetofholes
+-------------------
+ (,)
+ (1,2)
+ (3,4)
+ (5,6)
+ (7,8)
+(5 rows)
+
+create or replace function returnssetofholes() returns setof has_hole language plpgsql as
+$$
+declare r record;
+begin
+ return next r; -- fails, not assigned yet
+end$$;
+select returnssetofholes();
+ERROR: record "r" is not assigned yet
+DETAIL: The tuple structure of a not-yet-assigned record is indeterminate.
+CONTEXT: PL/pgSQL function returnssetofholes() line 4 at RETURN NEXT
+create or replace function returnssetofholes() returns setof has_hole language plpgsql as
+$$
+begin
+ return next row(1,2,3); -- fails
+end$$;
+select returnssetofholes();
+ERROR: returned record type does not match expected record type
+DETAIL: Number of returned columns (3) does not match expected column count (2).
+CONTEXT: PL/pgSQL function returnssetofholes() line 3 at RETURN NEXT
+-- check behavior with changes of a named rowtype
+create table mutable(f1 int, f2 text);
+create function sillyaddone(int) returns int language plpgsql as
+$$ declare r mutable; begin r.f1 := $1; return r.f1 + 1; end $$;
+select sillyaddone(42);
+ sillyaddone
+-------------
+ 43
+(1 row)
+
+alter table mutable drop column f1;
+alter table mutable add column f1 float8;
+-- currently, this fails due to cached plan for "r.f1 + 1" expression
+select sillyaddone(42);
+ERROR: type of parameter 4 (double precision) does not match that when preparing the plan (integer)
+CONTEXT: PL/pgSQL function sillyaddone(integer) line 1 at RETURN
+\c -
+-- but it's OK after a reconnect
+select sillyaddone(42);
+ sillyaddone
+-------------
+ 43
+(1 row)
+
+alter table mutable drop column f1;
+select sillyaddone(42); -- fail
+ERROR: record "r" has no field "f1"
+CONTEXT: PL/pgSQL function sillyaddone(integer) line 1 at assignment
+create function getf3(x mutable) returns int language plpgsql as
+$$ begin return x.f3; end $$;
+select getf3(null::mutable); -- doesn't work yet
+ERROR: record "x" has no field "f3"
+CONTEXT: SQL statement "SELECT x.f3"
+PL/pgSQL function getf3(mutable) line 1 at RETURN
+alter table mutable add column f3 int;
+select getf3(null::mutable); -- now it works
+ getf3
+-------
+
+(1 row)
+
+alter table mutable drop column f3;
+select getf3(null::mutable); -- fails again
+ERROR: record "x" has no field "f3"
+CONTEXT: PL/pgSQL function getf3(mutable) line 1 at RETURN
+-- check access to system columns in a record variable
+create function sillytrig() returns trigger language plpgsql as
+$$begin
+ raise notice 'old.ctid = %', old.ctid;
+ raise notice 'old.tableoid = %', old.tableoid::regclass;
+ return new;
+end$$;
+create trigger mutable_trig before update on mutable for each row
+execute procedure sillytrig();
+insert into mutable values ('foo'), ('bar');
+update mutable set f2 = f2 || ' baz';
+NOTICE: old.ctid = (0,1)
+NOTICE: old.tableoid = mutable
+NOTICE: old.ctid = (0,2)
+NOTICE: old.tableoid = mutable
+table mutable;
+ f2
+---------
+ foo baz
+ bar baz
+(2 rows)
+
+-- check returning a composite datum from a trigger
+create or replace function sillytrig() returns trigger language plpgsql as
+$$begin
+ return row(new.*);
+end$$;
+update mutable set f2 = f2 || ' baz';
+table mutable;
+ f2
+-------------
+ foo baz baz
+ bar baz baz
+(2 rows)
+
+create or replace function sillytrig() returns trigger language plpgsql as
+$$declare r record;
+begin
+ r := row(new.*);
+ return r;
+end$$;
+update mutable set f2 = f2 || ' baz';
+table mutable;
+ f2
+-----------------
+ foo baz baz baz
+ bar baz baz baz
+(2 rows)
+
+--
+-- Domains of composite
+--
+create domain ordered_int8s as two_int8s check((value).q1 <= (value).q2);
+create function read_ordered_int8s(p ordered_int8s) returns int8 as $$
+begin return p.q1 + p.q2; end
+$$ language plpgsql;
+select read_ordered_int8s(row(1, 2));
+ read_ordered_int8s
+--------------------
+ 3
+(1 row)
+
+select read_ordered_int8s(row(2, 1)); -- fail
+ERROR: value for domain ordered_int8s violates check constraint "ordered_int8s_check"
+create function build_ordered_int8s(i int8, j int8) returns ordered_int8s as $$
+begin return row(i,j); end
+$$ language plpgsql;
+select build_ordered_int8s(1,2);
+ build_ordered_int8s
+---------------------
+ (1,2)
+(1 row)
+
+select build_ordered_int8s(2,1); -- fail
+ERROR: value for domain ordered_int8s violates check constraint "ordered_int8s_check"
+CONTEXT: PL/pgSQL function build_ordered_int8s(bigint,bigint) while casting return value to function's return type
+create function build_ordered_int8s_2(i int8, j int8) returns ordered_int8s as $$
+declare r record; begin r := row(i,j); return r; end
+$$ language plpgsql;
+select build_ordered_int8s_2(1,2);
+ build_ordered_int8s_2
+-----------------------
+ (1,2)
+(1 row)
+
+select build_ordered_int8s_2(2,1); -- fail
+ERROR: value for domain ordered_int8s violates check constraint "ordered_int8s_check"
+CONTEXT: PL/pgSQL function build_ordered_int8s_2(bigint,bigint) while casting return value to function's return type
+create function build_ordered_int8s_3(i int8, j int8) returns ordered_int8s as $$
+declare r two_int8s; begin r := row(i,j); return r; end
+$$ language plpgsql;
+select build_ordered_int8s_3(1,2);
+ build_ordered_int8s_3
+-----------------------
+ (1,2)
+(1 row)
+
+select build_ordered_int8s_3(2,1); -- fail
+ERROR: value for domain ordered_int8s violates check constraint "ordered_int8s_check"
+CONTEXT: PL/pgSQL function build_ordered_int8s_3(bigint,bigint) while casting return value to function's return type
+create function build_ordered_int8s_4(i int8, j int8) returns ordered_int8s as $$
+declare r ordered_int8s; begin r := row(i,j); return r; end
+$$ language plpgsql;
+select build_ordered_int8s_4(1,2);
+ build_ordered_int8s_4
+-----------------------
+ (1,2)
+(1 row)
+
+select build_ordered_int8s_4(2,1); -- fail
+ERROR: value for domain ordered_int8s violates check constraint "ordered_int8s_check"
+CONTEXT: PL/pgSQL function build_ordered_int8s_4(bigint,bigint) line 2 at assignment
+create function build_ordered_int8s_a(i int8, j int8) returns ordered_int8s[] as $$
+begin return array[row(i,j), row(i,j+1)]; end
+$$ language plpgsql;
+select build_ordered_int8s_a(1,2);
+ build_ordered_int8s_a
+-----------------------
+ {"(1,2)","(1,3)"}
+(1 row)
+
+select build_ordered_int8s_a(2,1); -- fail
+ERROR: value for domain ordered_int8s violates check constraint "ordered_int8s_check"
+CONTEXT: PL/pgSQL function build_ordered_int8s_a(bigint,bigint) while casting return value to function's return type
+-- check field assignment
+do $$
+declare r ordered_int8s;
+begin
+ r.q1 := null;
+ r.q2 := 43;
+ r.q1 := 42;
+ r.q2 := 41; -- fail
+end$$;
+ERROR: value for domain ordered_int8s violates check constraint "ordered_int8s_check"
+CONTEXT: PL/pgSQL function inline_code_block line 7 at assignment
+-- check whole-row assignment
+do $$
+declare r ordered_int8s;
+begin
+ r := null;
+ r := row(null,null);
+ r := row(1,2);
+ r := row(2,1); -- fail
+end$$;
+ERROR: value for domain ordered_int8s violates check constraint "ordered_int8s_check"
+CONTEXT: PL/pgSQL function inline_code_block line 7 at assignment
+-- check assignment in for-loop
+do $$
+declare r ordered_int8s;
+begin
+ for r in values (1,2),(3,4),(6,5) loop
+ raise notice 'r = %', r;
+ end loop;
+end$$;
+NOTICE: r = (1,2)
+NOTICE: r = (3,4)
+ERROR: value for domain ordered_int8s violates check constraint "ordered_int8s_check"
+CONTEXT: PL/pgSQL function inline_code_block line 4 at FOR over SELECT rows
+-- check behavior with toastable fields, too
+create type two_texts as (f1 text, f2 text);
+create domain ordered_texts as two_texts check((value).f1 <= (value).f2);
+create table sometable (id int, a text, b text);
+-- b should be compressed, but in-line
+insert into sometable values (1, 'a', repeat('ffoob',1000));
+-- this b should be out-of-line
+insert into sometable values (2, 'a', repeat('ffoob',100000));
+-- this pair should fail the domain check
+insert into sometable values (3, 'z', repeat('ffoob',100000));
+do $$
+declare d ordered_texts;
+begin
+ for d in select a, b from sometable loop
+ raise notice 'succeeded at "%"', d.f1;
+ end loop;
+end$$;
+NOTICE: succeeded at "a"
+NOTICE: succeeded at "a"
+ERROR: value for domain ordered_texts violates check constraint "ordered_texts_check"
+CONTEXT: PL/pgSQL function inline_code_block line 4 at FOR over SELECT rows
+do $$
+declare r record; d ordered_texts;
+begin
+ for r in select * from sometable loop
+ raise notice 'processing row %', r.id;
+ d := row(r.a, r.b);
+ end loop;
+end$$;
+NOTICE: processing row 1
+NOTICE: processing row 2
+NOTICE: processing row 3
+ERROR: value for domain ordered_texts violates check constraint "ordered_texts_check"
+CONTEXT: PL/pgSQL function inline_code_block line 6 at assignment
+do $$
+declare r record; d ordered_texts;
+begin
+ for r in select * from sometable loop
+ raise notice 'processing row %', r.id;
+ d := null;
+ d.f1 := r.a;
+ d.f2 := r.b;
+ end loop;
+end$$;
+NOTICE: processing row 1
+NOTICE: processing row 2
+NOTICE: processing row 3
+ERROR: value for domain ordered_texts violates check constraint "ordered_texts_check"
+CONTEXT: PL/pgSQL function inline_code_block line 8 at assignment
#include "utils/regproc.h"
#include "utils/rel.h"
#include "utils/syscache.h"
+#include "utils/typcache.h"
#include "plpgsql.h"
static Node *resolve_column_ref(ParseState *pstate, PLpgSQL_expr *expr,
ColumnRef *cref, bool error_if_no_field);
static Node *make_datum_param(PLpgSQL_expr *expr, int dno, int location);
-static PLpgSQL_row *build_row_from_class(Oid classOid);
static PLpgSQL_row *build_row_from_vars(PLpgSQL_variable **vars, int numvars);
static PLpgSQL_type *build_datatype(HeapTuple typeTup, int32 typmod, Oid collation);
static void plpgsql_start_datums(void);
/* Disallow pseudotype argument */
/* (note we already replaced polymorphic types) */
/* (build_variable would do this, but wrong message) */
- if (argdtype->ttype != PLPGSQL_TTYPE_SCALAR &&
- argdtype->ttype != PLPGSQL_TTYPE_ROW)
+ if (argdtype->ttype == PLPGSQL_TTYPE_PSEUDO)
ereport(ERROR,
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
errmsg("PL/pgSQL functions cannot accept type %s",
}
else
{
- Assert(argvariable->dtype == PLPGSQL_DTYPE_ROW);
- argitemtype = PLPGSQL_NSTYPE_ROW;
+ Assert(argvariable->dtype == PLPGSQL_DTYPE_REC);
+ argitemtype = PLPGSQL_NSTYPE_REC;
}
/* Remember arguments in appropriate arrays */
format_type_be(rettypeid))));
}
- if (typeStruct->typrelid != InvalidOid ||
- rettypeid == RECORDOID)
- function->fn_retistuple = true;
- else
+ function->fn_retistuple = type_is_rowtype(rettypeid);
+ function->fn_retbyval = typeStruct->typbyval;
+ function->fn_rettyplen = typeStruct->typlen;
+
+ /*
+ * install $0 reference, but only for polymorphic return
+ * types, and not when the return is specified through an
+ * output parameter.
+ */
+ if (IsPolymorphicType(procStruct->prorettype) &&
+ num_out_args == 0)
{
- function->fn_retbyval = typeStruct->typbyval;
- function->fn_rettyplen = typeStruct->typlen;
-
- /*
- * install $0 reference, but only for polymorphic return
- * types, and not when the return is specified through an
- * output parameter.
- */
- if (IsPolymorphicType(procStruct->prorettype) &&
- num_out_args == 0)
- {
- (void) plpgsql_build_variable("$0", 0,
- build_datatype(typeTup,
- -1,
- function->fn_input_collation),
- true);
- }
+ (void) plpgsql_build_variable("$0", 0,
+ build_datatype(typeTup,
+ -1,
+ function->fn_input_collation),
+ true);
}
+
ReleaseSysCache(typeTup);
}
break;
errhint("The arguments of the trigger can be accessed through TG_NARGS and TG_ARGV instead.")));
/* Add the record for referencing NEW ROW */
- rec = plpgsql_build_record("new", 0, true);
+ rec = plpgsql_build_record("new", 0, RECORDOID, true);
function->new_varno = rec->dno;
/* Add the record for referencing OLD ROW */
- rec = plpgsql_build_record("old", 0, true);
+ rec = plpgsql_build_record("old", 0, RECORDOID, true);
function->old_varno = rec->dno;
/* Add the variable tg_name */
if (nnames == nnames_field)
{
/* colname could be a field in this record */
+ PLpgSQL_rec *rec = (PLpgSQL_rec *) estate->datums[nse->itemno];
int i;
/* search for a datum referencing this field */
- for (i = 0; i < estate->ndatums; i++)
+ i = rec->firstfield;
+ while (i >= 0)
{
PLpgSQL_recfield *fld = (PLpgSQL_recfield *) estate->datums[i];
- if (fld->dtype == PLPGSQL_DTYPE_RECFIELD &&
- fld->recparentno == nse->itemno &&
- strcmp(fld->fieldname, colname) == 0)
+ Assert(fld->dtype == PLPGSQL_DTYPE_RECFIELD &&
+ fld->recparentno == nse->itemno);
+ if (strcmp(fld->fieldname, colname) == 0)
{
return make_datum_param(expr, i, cref->location);
}
+ i = fld->nextfield;
}
/*
parser_errposition(pstate, cref->location)));
}
break;
- case PLPGSQL_NSTYPE_ROW:
- if (nnames == nnames_wholerow)
- return make_datum_param(expr, nse->itemno, cref->location);
- if (nnames == nnames_field)
- {
- /* colname could be a field in this row */
- PLpgSQL_row *row = (PLpgSQL_row *) estate->datums[nse->itemno];
- int i;
-
- for (i = 0; i < row->nfields; i++)
- {
- if (row->fieldnames[i] &&
- strcmp(row->fieldnames[i], colname) == 0)
- {
- return make_datum_param(expr, row->varnos[i],
- cref->location);
- }
- }
- /* Not found, so throw error or return NULL */
- if (error_if_no_field)
- ereport(ERROR,
- (errcode(ERRCODE_UNDEFINED_COLUMN),
- errmsg("record \"%s\" has no field \"%s\"",
- (nnames_field == 1) ? name1 : name2,
- colname),
- parser_errposition(pstate, cref->location)));
- }
- break;
default:
elog(ERROR, "unrecognized plpgsql itemtype: %d", nse->itemtype);
}
switch (ns->itemtype)
{
case PLPGSQL_NSTYPE_VAR:
- case PLPGSQL_NSTYPE_ROW:
case PLPGSQL_NSTYPE_REC:
wdatum->datum = plpgsql_Datums[ns->itemno];
wdatum->ident = word1;
* datum whether it is or not --- any error will be
* detected later.
*/
+ PLpgSQL_rec *rec;
PLpgSQL_recfield *new;
- new = palloc(sizeof(PLpgSQL_recfield));
- new->dtype = PLPGSQL_DTYPE_RECFIELD;
- new->fieldname = pstrdup(word2);
- new->recparentno = ns->itemno;
-
- plpgsql_adddatum((PLpgSQL_datum *) new);
+ rec = (PLpgSQL_rec *) (plpgsql_Datums[ns->itemno]);
+ new = plpgsql_build_recfield(rec, word2);
wdatum->datum = (PLpgSQL_datum *) new;
}
wdatum->idents = idents;
return true;
- case PLPGSQL_NSTYPE_ROW:
- if (nnames == 1)
- {
- /*
- * First word is a row name, so second word could be a
- * field in this row. Again, no error now if it
- * isn't.
- */
- PLpgSQL_row *row;
- int i;
-
- row = (PLpgSQL_row *) (plpgsql_Datums[ns->itemno]);
- for (i = 0; i < row->nfields; i++)
- {
- if (row->fieldnames[i] &&
- strcmp(row->fieldnames[i], word2) == 0)
- {
- wdatum->datum = plpgsql_Datums[row->varnos[i]];
- wdatum->ident = NULL;
- wdatum->quoted = false; /* not used */
- wdatum->idents = idents;
- return true;
- }
- }
- /* fall through to return CWORD */
- }
- else
- {
- /* Block-qualified reference to row variable. */
- wdatum->datum = plpgsql_Datums[ns->itemno];
- wdatum->ident = NULL;
- wdatum->quoted = false; /* not used */
- wdatum->idents = idents;
- return true;
- }
- break;
-
default:
break;
}
* words 1/2 are a record name, so third word could be
* a field in this record.
*/
+ PLpgSQL_rec *rec;
PLpgSQL_recfield *new;
- new = palloc(sizeof(PLpgSQL_recfield));
- new->dtype = PLPGSQL_DTYPE_RECFIELD;
- new->fieldname = pstrdup(word3);
- new->recparentno = ns->itemno;
-
- plpgsql_adddatum((PLpgSQL_datum *) new);
+ rec = (PLpgSQL_rec *) (plpgsql_Datums[ns->itemno]);
+ new = plpgsql_build_recfield(rec, word3);
wdatum->datum = (PLpgSQL_datum *) new;
wdatum->ident = NULL;
return true;
}
- case PLPGSQL_NSTYPE_ROW:
- {
- /*
- * words 1/2 are a row name, so third word could be a
- * field in this row.
- */
- PLpgSQL_row *row;
- int i;
-
- row = (PLpgSQL_row *) (plpgsql_Datums[ns->itemno]);
- for (i = 0; i < row->nfields; i++)
- {
- if (row->fieldnames[i] &&
- strcmp(row->fieldnames[i], word3) == 0)
- {
- wdatum->datum = plpgsql_Datums[row->varnos[i]];
- wdatum->ident = NULL;
- wdatum->quoted = false; /* not used */
- wdatum->idents = idents;
- return true;
- }
- }
- /* fall through to return CWORD */
- break;
- }
-
default:
break;
}
* plpgsql_build_variable - build a datum-array entry of a given
* datatype
*
- * The returned struct may be a PLpgSQL_var, PLpgSQL_row, or
- * PLpgSQL_rec depending on the given datatype, and is allocated via
+ * The returned struct may be a PLpgSQL_var or PLpgSQL_rec
+ * depending on the given datatype, and is allocated via
* palloc. The struct is automatically added to the current datum
* array, and optionally to the current namespace.
*/
result = (PLpgSQL_variable *) var;
break;
}
- case PLPGSQL_TTYPE_ROW:
- {
- /* Composite type -- build a row variable */
- PLpgSQL_row *row;
-
- row = build_row_from_class(dtype->typrelid);
-
- row->dtype = PLPGSQL_DTYPE_ROW;
- row->refname = pstrdup(refname);
- row->lineno = lineno;
-
- plpgsql_adddatum((PLpgSQL_datum *) row);
- if (add2namespace)
- plpgsql_ns_additem(PLPGSQL_NSTYPE_ROW,
- row->dno,
- refname);
- result = (PLpgSQL_variable *) row;
- break;
- }
case PLPGSQL_TTYPE_REC:
{
- /* "record" type -- build a record variable */
+ /* Composite type -- build a record variable */
PLpgSQL_rec *rec;
- rec = plpgsql_build_record(refname, lineno, add2namespace);
+ rec = plpgsql_build_record(refname, lineno, dtype->typoid,
+ add2namespace);
result = (PLpgSQL_variable *) rec;
break;
}
* Build empty named record variable, and optionally add it to namespace
*/
PLpgSQL_rec *
-plpgsql_build_record(const char *refname, int lineno, bool add2namespace)
+plpgsql_build_record(const char *refname, int lineno, Oid rectypeid,
+ bool add2namespace)
{
PLpgSQL_rec *rec;
rec->dtype = PLPGSQL_DTYPE_REC;
rec->refname = pstrdup(refname);
rec->lineno = lineno;
- rec->tup = NULL;
- rec->tupdesc = NULL;
- rec->freetup = false;
- rec->freetupdesc = false;
+ rec->rectypeid = rectypeid;
+ rec->firstfield = -1;
+ rec->erh = NULL;
plpgsql_adddatum((PLpgSQL_datum *) rec);
if (add2namespace)
plpgsql_ns_additem(PLPGSQL_NSTYPE_REC, rec->dno, rec->refname);
return rec;
}
-/*
- * Build a row-variable data structure given the pg_class OID.
- */
-static PLpgSQL_row *
-build_row_from_class(Oid classOid)
-{
- PLpgSQL_row *row;
- Relation rel;
- Form_pg_class classStruct;
- const char *relname;
- int i;
-
- /*
- * Open the relation to get info.
- */
- rel = relation_open(classOid, AccessShareLock);
- classStruct = RelationGetForm(rel);
- relname = RelationGetRelationName(rel);
-
- /*
- * Accept relation, sequence, view, materialized view, composite type, or
- * foreign table.
- */
- if (classStruct->relkind != RELKIND_RELATION &&
- classStruct->relkind != RELKIND_SEQUENCE &&
- classStruct->relkind != RELKIND_VIEW &&
- classStruct->relkind != RELKIND_MATVIEW &&
- classStruct->relkind != RELKIND_COMPOSITE_TYPE &&
- classStruct->relkind != RELKIND_FOREIGN_TABLE &&
- classStruct->relkind != RELKIND_PARTITIONED_TABLE)
- ereport(ERROR,
- (errcode(ERRCODE_WRONG_OBJECT_TYPE),
- errmsg("relation \"%s\" is not a table", relname)));
-
- /*
- * Create a row datum entry and all the required variables that it will
- * point to.
- */
- row = palloc0(sizeof(PLpgSQL_row));
- row->dtype = PLPGSQL_DTYPE_ROW;
- row->rowtupdesc = CreateTupleDescCopy(RelationGetDescr(rel));
- row->nfields = classStruct->relnatts;
- row->fieldnames = palloc(sizeof(char *) * row->nfields);
- row->varnos = palloc(sizeof(int) * row->nfields);
-
- for (i = 0; i < row->nfields; i++)
- {
- Form_pg_attribute attrStruct;
-
- /*
- * Get the attribute and check for dropped column
- */
- attrStruct = TupleDescAttr(row->rowtupdesc, i);
-
- if (!attrStruct->attisdropped)
- {
- char *attname;
- char refname[(NAMEDATALEN * 2) + 100];
- PLpgSQL_variable *var;
-
- attname = NameStr(attrStruct->attname);
- snprintf(refname, sizeof(refname), "%s.%s", relname, attname);
-
- /*
- * Create the internal variable for the field
- *
- * We know if the table definitions contain a default value or if
- * the field is declared in the table as NOT NULL. But it's
- * possible to create a table field as NOT NULL without a default
- * value and that would lead to problems later when initializing
- * the variables due to entering a block at execution time. Thus
- * we ignore this information for now.
- */
- var = plpgsql_build_variable(refname, 0,
- plpgsql_build_datatype(attrStruct->atttypid,
- attrStruct->atttypmod,
- attrStruct->attcollation),
- false);
-
- /* Add the variable to the row */
- row->fieldnames[i] = attname;
- row->varnos[i] = var->dno;
- }
- else
- {
- /* Leave a hole in the row structure for the dropped col */
- row->fieldnames[i] = NULL;
- row->varnos[i] = -1;
- }
- }
-
- relation_close(rel, AccessShareLock);
-
- return row;
-}
-
/*
* Build a row-variable data structure given the component variables.
+ * Include a rowtupdesc, since we will need to materialize the row result.
*/
static PLpgSQL_row *
build_row_from_vars(PLpgSQL_variable **vars, int numvars)
for (i = 0; i < numvars; i++)
{
PLpgSQL_variable *var = vars[i];
- Oid typoid = RECORDOID;
- int32 typmod = -1;
- Oid typcoll = InvalidOid;
+ Oid typoid;
+ int32 typmod;
+ Oid typcoll;
switch (var->dtype)
{
break;
case PLPGSQL_DTYPE_REC:
- break;
-
- case PLPGSQL_DTYPE_ROW:
- if (((PLpgSQL_row *) var)->rowtupdesc)
- {
- typoid = ((PLpgSQL_row *) var)->rowtupdesc->tdtypeid;
- typmod = ((PLpgSQL_row *) var)->rowtupdesc->tdtypmod;
- /* composite types have no collation */
- }
+ typoid = ((PLpgSQL_rec *) var)->rectypeid;
+ typmod = -1; /* don't know typmod, if it's used at all */
+ typcoll = InvalidOid; /* composite types have no collation */
break;
default:
elog(ERROR, "unrecognized dtype: %d", var->dtype);
+ typoid = InvalidOid; /* keep compiler quiet */
+ typmod = 0;
+ typcoll = InvalidOid;
+ break;
}
row->fieldnames[i] = var->refname;
return row;
}
+/*
+ * Build a RECFIELD datum for the named field of the specified record variable
+ *
+ * If there's already such a datum, just return it; we don't need duplicates.
+ */
+PLpgSQL_recfield *
+plpgsql_build_recfield(PLpgSQL_rec *rec, const char *fldname)
+{
+ PLpgSQL_recfield *recfield;
+ int i;
+
+ /* search for an existing datum referencing this field */
+ i = rec->firstfield;
+ while (i >= 0)
+ {
+ PLpgSQL_recfield *fld = (PLpgSQL_recfield *) plpgsql_Datums[i];
+
+ Assert(fld->dtype == PLPGSQL_DTYPE_RECFIELD &&
+ fld->recparentno == rec->dno);
+ if (strcmp(fld->fieldname, fldname) == 0)
+ return fld;
+ i = fld->nextfield;
+ }
+
+ /* nope, so make a new one */
+ recfield = palloc0(sizeof(PLpgSQL_recfield));
+ recfield->dtype = PLPGSQL_DTYPE_RECFIELD;
+ recfield->fieldname = pstrdup(fldname);
+ recfield->recparentno = rec->dno;
+ recfield->rectupledescid = INVALID_TUPLEDESC_IDENTIFIER;
+
+ plpgsql_adddatum((PLpgSQL_datum *) recfield);
+
+ /* now we can link it into the parent's chain */
+ recfield->nextfield = rec->firstfield;
+ rec->firstfield = recfield->dno;
+
+ return recfield;
+}
+
/*
* plpgsql_build_datatype
* Build PLpgSQL_type struct given type OID, typmod, and collation.
switch (typeStruct->typtype)
{
case TYPTYPE_BASE:
- case TYPTYPE_DOMAIN:
case TYPTYPE_ENUM:
case TYPTYPE_RANGE:
typ->ttype = PLPGSQL_TTYPE_SCALAR;
break;
case TYPTYPE_COMPOSITE:
- Assert(OidIsValid(typeStruct->typrelid));
- typ->ttype = PLPGSQL_TTYPE_ROW;
+ typ->ttype = PLPGSQL_TTYPE_REC;
+ break;
+ case TYPTYPE_DOMAIN:
+ if (type_is_rowtype(typeStruct->typbasetype))
+ typ->ttype = PLPGSQL_TTYPE_REC;
+ else
+ typ->ttype = PLPGSQL_TTYPE_SCALAR;
break;
case TYPTYPE_PSEUDO:
if (typ->typoid == RECORDOID)
typ->typlen = typeStruct->typlen;
typ->typbyval = typeStruct->typbyval;
typ->typtype = typeStruct->typtype;
- typ->typrelid = typeStruct->typrelid;
typ->collation = typeStruct->typcollation;
if (OidIsValid(collation) && OidIsValid(typ->collation))
typ->collation = collation;
/************************************************************
* Local function forward declarations
************************************************************/
+static void coerce_function_result_tuple(PLpgSQL_execstate *estate,
+ TupleDesc tupdesc);
static void plpgsql_exec_error_callback(void *arg);
static PLpgSQL_datum *copy_plpgsql_datum(PLpgSQL_datum *datum);
static MemoryContext get_stmt_mcontext(PLpgSQL_execstate *estate);
static int exec_stmt_dynfors(PLpgSQL_execstate *estate,
PLpgSQL_stmt_dynfors *stmt);
static int exec_stmt_commit(PLpgSQL_execstate *estate,
- PLpgSQL_stmt_commit *stmt);
+ PLpgSQL_stmt_commit *stmt);
static int exec_stmt_rollback(PLpgSQL_execstate *estate,
- PLpgSQL_stmt_rollback *stmt);
+ PLpgSQL_stmt_rollback *stmt);
static void plpgsql_estate_setup(PLpgSQL_execstate *estate,
PLpgSQL_function *func,
PLpgSQL_expr *expr);
static ParamExternData *plpgsql_param_fetch(ParamListInfo params,
int paramid, bool speculative,
- ParamExternData *prm);
+ ParamExternData *workspace);
static void plpgsql_param_compile(ParamListInfo params, Param *param,
ExprState *state,
Datum *resv, bool *resnull);
ExprContext *econtext);
static void plpgsql_param_eval_var_ro(ExprState *state, ExprEvalStep *op,
ExprContext *econtext);
-static void plpgsql_param_eval_non_var(ExprState *state, ExprEvalStep *op,
+static void plpgsql_param_eval_recfield(ExprState *state, ExprEvalStep *op,
+ ExprContext *econtext);
+static void plpgsql_param_eval_generic(ExprState *state, ExprEvalStep *op,
ExprContext *econtext);
+static void plpgsql_param_eval_generic_ro(ExprState *state, ExprEvalStep *op,
+ ExprContext *econtext);
static void exec_move_row(PLpgSQL_execstate *estate,
PLpgSQL_variable *target,
HeapTuple tup, TupleDesc tupdesc);
+static ExpandedRecordHeader *make_expanded_record_for_rec(PLpgSQL_execstate *estate,
+ PLpgSQL_rec *rec,
+ TupleDesc srctupdesc,
+ ExpandedRecordHeader *srcerh);
+static void exec_move_row_from_fields(PLpgSQL_execstate *estate,
+ PLpgSQL_variable *target,
+ ExpandedRecordHeader *newerh,
+ Datum *values, bool *nulls,
+ TupleDesc tupdesc);
+static bool compatible_tupdescs(TupleDesc src_tupdesc, TupleDesc dst_tupdesc);
static HeapTuple make_tuple_from_row(PLpgSQL_execstate *estate,
PLpgSQL_row *row,
TupleDesc tupdesc);
-static HeapTuple get_tuple_from_datum(Datum value);
-static TupleDesc get_tupdesc_from_datum(Datum value);
+static TupleDesc deconstruct_composite_datum(Datum value,
+ HeapTupleData *tmptup);
static void exec_move_row_from_datum(PLpgSQL_execstate *estate,
PLpgSQL_variable *target,
Datum value);
+static void instantiate_empty_record_variable(PLpgSQL_execstate *estate,
+ PLpgSQL_rec *rec);
static char *convert_value_to_string(PLpgSQL_execstate *estate,
Datum value, Oid valtype);
static Datum exec_cast_value(PLpgSQL_execstate *estate,
Datum newvalue, bool isnull, bool freeable);
static void assign_text_var(PLpgSQL_execstate *estate, PLpgSQL_var *var,
const char *str);
+static void assign_record_var(PLpgSQL_execstate *estate, PLpgSQL_rec *rec,
+ ExpandedRecordHeader *erh);
static PreparedParamsData *exec_eval_using_params(PLpgSQL_execstate *estate,
List *params);
static Portal exec_dynquery_with_params(PLpgSQL_execstate *estate,
/* take ownership of R/W object */
assign_simple_var(&estate, var,
TransferExpandedObject(var->value,
- CurrentMemoryContext),
+ estate.datum_context),
false,
true);
}
/* flat array, so force to expanded form */
assign_simple_var(&estate, var,
expand_array(var->value,
- CurrentMemoryContext,
+ estate.datum_context,
NULL),
false,
true);
}
break;
- case PLPGSQL_DTYPE_ROW:
+ case PLPGSQL_DTYPE_REC:
{
- PLpgSQL_row *row = (PLpgSQL_row *) estate.datums[n];
+ PLpgSQL_rec *rec = (PLpgSQL_rec *) estate.datums[n];
if (!fcinfo->argnull[i])
{
/* Assign row value from composite datum */
exec_move_row_from_datum(&estate,
- (PLpgSQL_variable *) row,
+ (PLpgSQL_variable *) rec,
fcinfo->arg[i]);
}
else
{
/* If arg is null, treat it as an empty row */
- exec_move_row(&estate, (PLpgSQL_variable *) row,
+ exec_move_row(&estate, (PLpgSQL_variable *) rec,
NULL, NULL);
}
/* clean up after exec_move_row() */
/* If we produced any tuples, send back the result */
if (estate.tuple_store)
{
- rsi->setResult = estate.tuple_store;
- if (estate.rettupdesc)
- {
- MemoryContext oldcxt;
+ MemoryContext oldcxt;
- oldcxt = MemoryContextSwitchTo(estate.tuple_store_cxt);
- rsi->setDesc = CreateTupleDescCopy(estate.rettupdesc);
- MemoryContextSwitchTo(oldcxt);
- }
+ rsi->setResult = estate.tuple_store;
+ oldcxt = MemoryContextSwitchTo(estate.tuple_store_cxt);
+ rsi->setDesc = CreateTupleDescCopy(estate.tuple_store_desc);
+ MemoryContextSwitchTo(oldcxt);
}
estate.retval = (Datum) 0;
fcinfo->isnull = true;
else if (!estate.retisnull)
{
if (!func->fn_rettype)
- {
ereport(ERROR,
(errmsg("cannot return a value from a procedure")));
- }
+ /*
+ * Cast result value to function's declared result type, and copy it
+ * out to the upper executor memory context. We must treat tuple
+ * results specially in order to deal with cases like rowtypes
+ * involving dropped columns.
+ */
if (estate.retistuple)
{
- /*
- * We have to check that the returned tuple actually matches the
- * expected result type. XXX would be better to cache the tupdesc
- * instead of repeating get_call_result_type()
- */
- HeapTuple rettup = (HeapTuple) DatumGetPointer(estate.retval);
- TupleDesc tupdesc;
- TupleConversionMap *tupmap;
-
- switch (get_call_result_type(fcinfo, NULL, &tupdesc))
+ /* Don't need coercion if rowtype is known to match */
+ if (func->fn_rettype == estate.rettype &&
+ func->fn_rettype != RECORDOID)
{
- case TYPEFUNC_COMPOSITE:
- /* got the expected result rowtype, now check it */
- tupmap = convert_tuples_by_position(estate.rettupdesc,
- tupdesc,
- gettext_noop("returned record type does not match expected record type"));
- /* it might need conversion */
- if (tupmap)
- rettup = do_convert_tuple(rettup, tupmap);
- /* no need to free map, we're about to return anyway */
- break;
- case TYPEFUNC_RECORD:
+ /*
+ * Copy the tuple result into upper executor memory context.
+ * However, if we have a R/W expanded datum, we can just
+ * transfer its ownership out to the upper context.
+ */
+ estate.retval = SPI_datumTransfer(estate.retval,
+ false,
+ -1);
+ }
+ else
+ {
+ /*
+ * Need to look up the expected result type. XXX would be
+ * better to cache the tupdesc instead of repeating
+ * get_call_result_type(), but the only easy place to save it
+ * is in the PLpgSQL_function struct, and that's too
+ * long-lived: composite types could change during the
+ * existence of a PLpgSQL_function.
+ */
+ Oid resultTypeId;
+ TupleDesc tupdesc;
- /*
- * Failed to determine actual type of RECORD. We could
- * raise an error here, but what this means in practice is
- * that the caller is expecting any old generic rowtype,
- * so we don't really need to be restrictive. Pass back
- * the generated result type, instead.
- */
- tupdesc = estate.rettupdesc;
- if (tupdesc == NULL) /* shouldn't happen */
+ switch (get_call_result_type(fcinfo, &resultTypeId, &tupdesc))
+ {
+ case TYPEFUNC_COMPOSITE:
+ /* got the expected result rowtype, now coerce it */
+ coerce_function_result_tuple(&estate, tupdesc);
+ break;
+ case TYPEFUNC_COMPOSITE_DOMAIN:
+ /* got the expected result rowtype, now coerce it */
+ coerce_function_result_tuple(&estate, tupdesc);
+ /* and check domain constraints */
+ /* XXX allowing caching here would be good, too */
+ domain_check(estate.retval, false, resultTypeId,
+ NULL, NULL);
+ break;
+ case TYPEFUNC_RECORD:
+
+ /*
+ * Failed to determine actual type of RECORD. We
+ * could raise an error here, but what this means in
+ * practice is that the caller is expecting any old
+ * generic rowtype, so we don't really need to be
+ * restrictive. Pass back the generated result as-is.
+ */
+ estate.retval = SPI_datumTransfer(estate.retval,
+ false,
+ -1);
+ break;
+ default:
+ /* shouldn't get here if retistuple is true ... */
elog(ERROR, "return type must be a row type");
- break;
- default:
- /* shouldn't get here if retistuple is true ... */
- elog(ERROR, "return type must be a row type");
- break;
+ break;
+ }
}
-
- /*
- * Copy tuple to upper executor memory, as a tuple Datum. Make
- * sure it is labeled with the caller-supplied tuple type.
- */
- estate.retval = PointerGetDatum(SPI_returntuple(rettup, tupdesc));
}
else
{
- /* Cast value to proper type */
+ /* Scalar case: use exec_cast_value */
estate.retval = exec_cast_value(&estate,
estate.retval,
&fcinfo->isnull,
return estate.retval;
}
+/*
+ * Helper for plpgsql_exec_function: coerce composite result to the specified
+ * tuple descriptor, and copy it out to upper executor memory. This is split
+ * out mostly for cosmetic reasons --- the logic would be very deeply nested
+ * otherwise.
+ *
+ * estate->retval is updated in-place.
+ */
+static void
+coerce_function_result_tuple(PLpgSQL_execstate *estate, TupleDesc tupdesc)
+{
+ HeapTuple rettup;
+ TupleDesc retdesc;
+ TupleConversionMap *tupmap;
+
+ /* We assume exec_stmt_return verified that result is composite */
+ Assert(type_is_rowtype(estate->rettype));
+
+ /* We can special-case expanded records for speed */
+ if (VARATT_IS_EXTERNAL_EXPANDED(DatumGetPointer(estate->retval)))
+ {
+ ExpandedRecordHeader *erh = (ExpandedRecordHeader *) DatumGetEOHP(estate->retval);
+
+ Assert(erh->er_magic == ER_MAGIC);
+
+ /* Extract record's TupleDesc */
+ retdesc = expanded_record_get_tupdesc(erh);
+
+ /* check rowtype compatibility */
+ tupmap = convert_tuples_by_position(retdesc,
+ tupdesc,
+ gettext_noop("returned record type does not match expected record type"));
+
+ /* it might need conversion */
+ if (tupmap)
+ {
+ rettup = expanded_record_get_tuple(erh);
+ Assert(rettup);
+ rettup = do_convert_tuple(rettup, tupmap);
+
+ /*
+ * Copy tuple to upper executor memory, as a tuple Datum. Make
+ * sure it is labeled with the caller-supplied tuple type.
+ */
+ estate->retval = PointerGetDatum(SPI_returntuple(rettup, tupdesc));
+ /* no need to free map, we're about to return anyway */
+ }
+ else
+ {
+ /*
+ * We need only copy result into upper executor memory context.
+ * However, if we have a R/W expanded datum, we can just transfer
+ * its ownership out to the upper executor context.
+ */
+ estate->retval = SPI_datumTransfer(estate->retval,
+ false,
+ -1);
+ }
+ }
+ else
+ {
+ /* Convert composite datum to a HeapTuple and TupleDesc */
+ HeapTupleData tmptup;
+
+ retdesc = deconstruct_composite_datum(estate->retval, &tmptup);
+ rettup = &tmptup;
+
+ /* check rowtype compatibility */
+ tupmap = convert_tuples_by_position(retdesc,
+ tupdesc,
+ gettext_noop("returned record type does not match expected record type"));
+
+ /* it might need conversion */
+ if (tupmap)
+ rettup = do_convert_tuple(rettup, tupmap);
+
+ /*
+ * Copy tuple to upper executor memory, as a tuple Datum. Make sure
+ * it is labeled with the caller-supplied tuple type.
+ */
+ estate->retval = PointerGetDatum(SPI_returntuple(rettup, tupdesc));
+
+ /* no need to free map, we're about to return anyway */
+
+ ReleaseTupleDesc(retdesc);
+ }
+}
+
/* ----------
* plpgsql_exec_trigger Called by the call handler for
ErrorContextCallback plerrcontext;
int i;
int rc;
+ TupleDesc tupdesc;
PLpgSQL_var *var;
PLpgSQL_rec *rec_new,
*rec_old;
* might have a test like "if (TG_OP = 'INSERT' and NEW.foo = 'xyz')",
* which should parse regardless of the current trigger type.
*/
+ tupdesc = RelationGetDescr(trigdata->tg_relation);
+
rec_new = (PLpgSQL_rec *) (estate.datums[func->new_varno]);
- rec_new->freetup = false;
- rec_new->tupdesc = trigdata->tg_relation->rd_att;
- rec_new->freetupdesc = false;
rec_old = (PLpgSQL_rec *) (estate.datums[func->old_varno]);
- rec_old->freetup = false;
- rec_old->tupdesc = trigdata->tg_relation->rd_att;
- rec_old->freetupdesc = false;
+
+ rec_new->erh = make_expanded_record_from_tupdesc(tupdesc,
+ estate.datum_context);
+ rec_old->erh = make_expanded_record_from_exprecord(rec_new->erh,
+ estate.datum_context);
if (!TRIGGER_FIRED_FOR_ROW(trigdata->tg_event))
{
/*
* Per-statement triggers don't use OLD/NEW variables
*/
- rec_new->tup = NULL;
- rec_old->tup = NULL;
}
else if (TRIGGER_FIRED_BY_INSERT(trigdata->tg_event))
{
- rec_new->tup = trigdata->tg_trigtuple;
- rec_old->tup = NULL;
+ expanded_record_set_tuple(rec_new->erh, trigdata->tg_trigtuple, false);
}
else if (TRIGGER_FIRED_BY_UPDATE(trigdata->tg_event))
{
- rec_new->tup = trigdata->tg_newtuple;
- rec_old->tup = trigdata->tg_trigtuple;
+ expanded_record_set_tuple(rec_new->erh, trigdata->tg_newtuple, false);
+ expanded_record_set_tuple(rec_old->erh, trigdata->tg_trigtuple, false);
}
else if (TRIGGER_FIRED_BY_DELETE(trigdata->tg_event))
{
- rec_new->tup = NULL;
- rec_old->tup = trigdata->tg_trigtuple;
+ expanded_record_set_tuple(rec_old->erh, trigdata->tg_trigtuple, false);
}
else
elog(ERROR, "unrecognized trigger action: not INSERT, DELETE, or UPDATE");
rettup = NULL;
else
{
+ TupleDesc retdesc;
TupleConversionMap *tupmap;
- rettup = (HeapTuple) DatumGetPointer(estate.retval);
- /* check rowtype compatibility */
- tupmap = convert_tuples_by_position(estate.rettupdesc,
- trigdata->tg_relation->rd_att,
- gettext_noop("returned row structure does not match the structure of the triggering table"));
- /* it might need conversion */
- if (tupmap)
- rettup = do_convert_tuple(rettup, tupmap);
- /* no need to free map, we're about to return anyway */
+ /* We assume exec_stmt_return verified that result is composite */
+ Assert(type_is_rowtype(estate.rettype));
- /* Copy tuple to upper executor memory */
- rettup = SPI_copytuple(rettup);
+ /* We can special-case expanded records for speed */
+ if (VARATT_IS_EXTERNAL_EXPANDED(DatumGetPointer(estate.retval)))
+ {
+ ExpandedRecordHeader *erh = (ExpandedRecordHeader *) DatumGetEOHP(estate.retval);
+
+ Assert(erh->er_magic == ER_MAGIC);
+
+ /* Extract HeapTuple and TupleDesc */
+ rettup = expanded_record_get_tuple(erh);
+ Assert(rettup);
+ retdesc = expanded_record_get_tupdesc(erh);
+
+ if (retdesc != RelationGetDescr(trigdata->tg_relation))
+ {
+ /* check rowtype compatibility */
+ tupmap = convert_tuples_by_position(retdesc,
+ RelationGetDescr(trigdata->tg_relation),
+ gettext_noop("returned row structure does not match the structure of the triggering table"));
+ /* it might need conversion */
+ if (tupmap)
+ rettup = do_convert_tuple(rettup, tupmap);
+ /* no need to free map, we're about to return anyway */
+ }
+
+ /*
+ * Copy tuple to upper executor memory. But if user just did
+ * "return new" or "return old" without changing anything, there's
+ * no need to copy; we can return the original tuple (which will
+ * save a few cycles in trigger.c as well as here).
+ */
+ if (rettup != trigdata->tg_newtuple &&
+ rettup != trigdata->tg_trigtuple)
+ rettup = SPI_copytuple(rettup);
+ }
+ else
+ {
+ /* Convert composite datum to a HeapTuple and TupleDesc */
+ HeapTupleData tmptup;
+
+ retdesc = deconstruct_composite_datum(estate.retval, &tmptup);
+ rettup = &tmptup;
+
+ /* check rowtype compatibility */
+ tupmap = convert_tuples_by_position(retdesc,
+ RelationGetDescr(trigdata->tg_relation),
+ gettext_noop("returned row structure does not match the structure of the triggering table"));
+ /* it might need conversion */
+ if (tupmap)
+ rettup = do_convert_tuple(rettup, tupmap);
+
+ ReleaseTupleDesc(retdesc);
+ /* no need to free map, we're about to return anyway */
+
+ /* Copy tuple to upper executor memory */
+ rettup = SPI_copytuple(rettup);
+ }
}
/*
PLpgSQL_rec *new = palloc(sizeof(PLpgSQL_rec));
memcpy(new, datum, sizeof(PLpgSQL_rec));
- /* should be preset to null/non-freeable */
- Assert(new->tup == NULL);
- Assert(new->tupdesc == NULL);
- Assert(!new->freetup);
- Assert(!new->freetupdesc);
+ /* should be preset to empty */
+ Assert(new->erh == NULL);
result = (PLpgSQL_datum *) new;
}
/*
* These datum records are read-only at runtime, so no need to
- * copy them (well, ARRAYELEM contains some cached type data, but
- * we'd just as soon centralize the caching anyway)
+ * copy them (well, RECFIELD and ARRAYELEM contain cached data,
+ * but we'd just as soon centralize the caching anyway)
*/
result = datum;
break;
{
PLpgSQL_rec *rec = (PLpgSQL_rec *) datum;
- if (rec->freetup)
- {
- heap_freetuple(rec->tup);
- rec->freetup = false;
- }
- if (rec->freetupdesc)
- {
- FreeTupleDesc(rec->tupdesc);
- rec->freetupdesc = false;
- }
- rec->tup = NULL;
- rec->tupdesc = NULL;
+ if (rec->erh)
+ DeleteExpandedObject(ExpandedRecordGetDatum(rec->erh));
+ rec->erh = NULL;
}
break;
/*
* If the block ended with RETURN, we may need to copy the return
- * value out of the subtransaction eval_context. This is
- * currently only needed for scalar result types --- rowtype
- * values will always exist in the function's main memory context,
- * cf. exec_stmt_return(). We can avoid a physical copy if the
- * value happens to be a R/W expanded object.
+ * value out of the subtransaction eval_context. We can avoid a
+ * physical copy if the value happens to be a R/W expanded object.
*/
if (rc == PLPGSQL_RC_RETURN &&
!estate->retisset &&
- !estate->retisnull &&
- estate->rettupdesc == NULL)
+ !estate->retisnull)
{
int16 resTypLen;
bool resTypByVal;
* exec_stmt_return Evaluate an expression and start
* returning from the function.
*
- * Note: in the retistuple code paths, the returned tuple is always in the
- * function's main context, whereas for non-tuple data types the result may
- * be in the eval_mcontext. The former case is not a memory leak since we're
- * about to exit the function anyway. (If you want to change it, note that
- * exec_stmt_block() knows about this behavior.) The latter case means that
- * we must not do exec_eval_cleanup while unwinding the control stack.
+ * Note: The result may be in the eval_mcontext. Therefore, we must not
+ * do exec_eval_cleanup while unwinding the control stack.
* ----------
*/
static int
if (estate->retisset)
return PLPGSQL_RC_RETURN;
- /* initialize for null result (possibly a tuple) */
+ /* initialize for null result */
estate->retval = (Datum) 0;
- estate->rettupdesc = NULL;
estate->retisnull = true;
estate->rettype = InvalidOid;
estate->rettype = var->datatype->typoid;
/*
- * Cope with retistuple case. A PLpgSQL_var could not be
- * of composite type, so we needn't make any effort to
- * convert. However, for consistency with the expression
- * code path, don't throw error if the result is NULL.
+ * A PLpgSQL_var could not be of composite type, so
+ * conversion must fail if retistuple. We throw a custom
+ * error mainly for consistency with historical behavior.
+ * For the same reason, we don't throw error if the result
+ * is NULL. (Note that plpgsql_exec_trigger assumes that
+ * any non-null result has been verified to be composite.)
*/
if (estate->retistuple && !estate->retisnull)
ereport(ERROR,
case PLPGSQL_DTYPE_REC:
{
PLpgSQL_rec *rec = (PLpgSQL_rec *) retvar;
- int32 rettypmod;
- if (HeapTupleIsValid(rec->tup))
+ /* If record is empty, we return NULL not a row of nulls */
+ if (rec->erh && !ExpandedRecordIsEmpty(rec->erh))
{
- if (estate->retistuple)
- {
- estate->retval = PointerGetDatum(rec->tup);
- estate->rettupdesc = rec->tupdesc;
- estate->retisnull = false;
- }
- else
- exec_eval_datum(estate,
- retvar,
- &estate->rettype,
- &rettypmod,
- &estate->retval,
- &estate->retisnull);
+ estate->retval = ExpandedRecordGetDatum(rec->erh);
+ estate->retisnull = false;
+ estate->rettype = rec->rectypeid;
}
}
break;
PLpgSQL_row *row = (PLpgSQL_row *) retvar;
int32 rettypmod;
- if (estate->retistuple)
- {
- HeapTuple tup;
-
- if (!row->rowtupdesc) /* should not happen */
- elog(ERROR, "row variable has no 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");
- estate->retval = PointerGetDatum(tup);
- estate->rettupdesc = row->rowtupdesc;
- estate->retisnull = false;
- }
- else
- exec_eval_datum(estate,
- retvar,
- &estate->rettype,
- &rettypmod,
- &estate->retval,
- &estate->retisnull);
+ /* We get here if there are multiple OUT parameters */
+ exec_eval_datum(estate,
+ (PLpgSQL_datum *) row,
+ &estate->rettype,
+ &rettypmod,
+ &estate->retval,
+ &estate->retisnull);
}
break;
&(estate->rettype),
&rettypmod);
- if (estate->retistuple && !estate->retisnull)
- {
- /* Convert composite datum to a HeapTuple and TupleDesc */
- HeapTuple tuple;
- TupleDesc tupdesc;
-
- /* Source must be of RECORD or composite type */
- if (!type_is_rowtype(estate->rettype))
- ereport(ERROR,
- (errcode(ERRCODE_DATATYPE_MISMATCH),
- errmsg("cannot return non-composite value from function returning composite type")));
- tuple = get_tuple_from_datum(estate->retval);
- tupdesc = get_tupdesc_from_datum(estate->retval);
- estate->retval = PointerGetDatum(tuple);
- estate->rettupdesc = CreateTupleDescCopy(tupdesc);
- ReleaseTupleDesc(tupdesc);
- }
+ /*
+ * As in the DTYPE_VAR case above, throw a custom error if a non-null,
+ * non-composite value is returned in a function returning tuple.
+ */
+ if (estate->retistuple && !estate->retisnull &&
+ !type_is_rowtype(estate->rettype))
+ ereport(ERROR,
+ (errcode(ERRCODE_DATATYPE_MISMATCH),
+ errmsg("cannot return non-composite value from function returning composite type")));
return PLPGSQL_RC_RETURN;
}
if (estate->tuple_store == NULL)
exec_init_tuple_store(estate);
- /* rettupdesc will be filled by exec_init_tuple_store */
- tupdesc = estate->rettupdesc;
+ /* tuple_store_desc will be filled by exec_init_tuple_store */
+ tupdesc = estate->tuple_store_desc;
natts = tupdesc->natts;
/*
case PLPGSQL_DTYPE_REC:
{
PLpgSQL_rec *rec = (PLpgSQL_rec *) retvar;
+ TupleDesc rec_tupdesc;
TupleConversionMap *tupmap;
- if (!HeapTupleIsValid(rec->tup))
- ereport(ERROR,
- (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
- errmsg("record \"%s\" is not assigned yet",
- rec->refname),
- errdetail("The tuple structure of a not-yet-assigned"
- " record is indeterminate.")));
+ /* If rec is null, try to convert it to a row of nulls */
+ if (rec->erh == NULL)
+ instantiate_empty_record_variable(estate, rec);
+ if (ExpandedRecordIsEmpty(rec->erh))
+ deconstruct_expanded_record(rec->erh);
/* Use eval_mcontext for tuple conversion work */
oldcontext = MemoryContextSwitchTo(get_eval_mcontext(estate));
- tupmap = convert_tuples_by_position(rec->tupdesc,
+ rec_tupdesc = expanded_record_get_tupdesc(rec->erh);
+ tupmap = convert_tuples_by_position(rec_tupdesc,
tupdesc,
gettext_noop("wrong record type supplied in RETURN NEXT"));
- tuple = rec->tup;
+ tuple = expanded_record_get_tuple(rec->erh);
if (tupmap)
tuple = do_convert_tuple(tuple, tupmap);
tuplestore_puttuple(estate->tuple_store, tuple);
{
PLpgSQL_row *row = (PLpgSQL_row *) retvar;
+ /* We get here if there are multiple OUT parameters */
+
/* Use eval_mcontext for tuple conversion work */
oldcontext = MemoryContextSwitchTo(get_eval_mcontext(estate));
tuple = make_tuple_from_row(estate, row, tupdesc);
- if (tuple == NULL)
+ if (tuple == NULL) /* should not happen */
ereport(ERROR,
(errcode(ERRCODE_DATATYPE_MISMATCH),
errmsg("wrong record type supplied in RETURN NEXT")));
/* Expression should be of RECORD or composite type */
if (!isNull)
{
+ HeapTupleData tmptup;
TupleDesc retvaldesc;
TupleConversionMap *tupmap;
/* Use eval_mcontext for tuple conversion work */
oldcontext = MemoryContextSwitchTo(get_eval_mcontext(estate));
- tuple = get_tuple_from_datum(retval);
- retvaldesc = get_tupdesc_from_datum(retval);
+ retvaldesc = deconstruct_composite_datum(retval, &tmptup);
+ tuple = &tmptup;
tupmap = convert_tuples_by_position(retvaldesc, tupdesc,
gettext_noop("returned record type does not match expected record type"));
if (tupmap)
oldcontext = MemoryContextSwitchTo(get_eval_mcontext(estate));
tupmap = convert_tuples_by_position(portal->tupDesc,
- estate->rettupdesc,
+ estate->tuple_store_desc,
gettext_noop("structure of query does not match function result type"));
while (true)
CurrentResourceOwner = oldowner;
MemoryContextSwitchTo(oldcxt);
- estate->rettupdesc = rsi->expectedDesc;
+ estate->tuple_store_desc = rsi->expectedDesc;
}
#define SET_RAISE_OPTION_TEXT(opt, name) \
estate->readonly_func = func->fn_readonly;
- estate->rettupdesc = NULL;
estate->exitlabel = NULL;
estate->cur_error = NULL;
estate->tuple_store = NULL;
+ estate->tuple_store_desc = NULL;
if (rsi)
{
estate->tuple_store_cxt = rsi->econtext->ecxt_per_query_memory;
estate->ndatums = func->ndatums;
estate->datums = palloc(sizeof(PLpgSQL_datum *) * estate->ndatums);
/* caller is expected to fill the datums array */
+ estate->datum_context = CurrentMemoryContext;
/* initialize our ParamListInfo with appropriate hook functions */
estate->paramLI = (ParamListInfo)
{
/* array and not already R/W, so apply expand_array */
newvalue = expand_array(newvalue,
- CurrentMemoryContext,
+ estate->datum_context,
NULL);
}
else
*/
PLpgSQL_recfield *recfield = (PLpgSQL_recfield *) target;
PLpgSQL_rec *rec;
- int fno;
- HeapTuple newtup;
- int colnums[1];
- Datum values[1];
- bool nulls[1];
- Oid atttype;
- int32 atttypmod;
+ ExpandedRecordHeader *erh;
rec = (PLpgSQL_rec *) (estate->datums[recfield->recparentno]);
+ erh = rec->erh;
/*
- * Check that there is already a tuple in the record. We need
- * that because records don't have any predefined field
- * structure.
- */
- if (!HeapTupleIsValid(rec->tup))
- ereport(ERROR,
- (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
- errmsg("record \"%s\" is not assigned yet",
- rec->refname),
- errdetail("The tuple structure of a not-yet-assigned record is indeterminate.")));
-
- /*
- * Get the number of the record field to change. Disallow
- * system columns because the code below won't cope.
+ * If record variable is NULL, instantiate it if it has a
+ * named composite type, else complain. (This won't change
+ * the logical state of the record, but if we successfully
+ * assign below, the unassigned fields will all become NULLs.)
*/
- fno = SPI_fnumber(rec->tupdesc, recfield->fieldname);
- if (fno <= 0)
- ereport(ERROR,
- (errcode(ERRCODE_UNDEFINED_COLUMN),
- errmsg("record \"%s\" has no field \"%s\"",
- rec->refname, recfield->fieldname)));
- colnums[0] = fno;
+ if (erh == NULL)
+ {
+ instantiate_empty_record_variable(estate, rec);
+ erh = rec->erh;
+ }
/*
- * Now insert the new value, being careful to cast it to the
- * right type.
+ * Look up the field's properties if we have not already, or
+ * if the tuple descriptor ID changed since last time.
*/
- atttype = TupleDescAttr(rec->tupdesc, fno - 1)->atttypid;
- atttypmod = TupleDescAttr(rec->tupdesc, fno - 1)->atttypmod;
- values[0] = exec_cast_value(estate,
- value,
- &isNull,
- valtype,
- valtypmod,
- atttype,
- atttypmod);
- nulls[0] = isNull;
-
- newtup = heap_modify_tuple_by_cols(rec->tup, rec->tupdesc,
- 1, colnums, values, nulls);
-
- if (rec->freetup)
- heap_freetuple(rec->tup);
-
- rec->tup = newtup;
- rec->freetup = true;
+ if (unlikely(recfield->rectupledescid != erh->er_tupdesc_id))
+ {
+ if (!expanded_record_lookup_field(erh,
+ recfield->fieldname,
+ &recfield->finfo))
+ ereport(ERROR,
+ (errcode(ERRCODE_UNDEFINED_COLUMN),
+ errmsg("record \"%s\" has no field \"%s\"",
+ rec->refname, recfield->fieldname)));
+ recfield->rectupledescid = erh->er_tupdesc_id;
+ }
+ /* We don't support assignments to system columns. */
+ if (recfield->finfo.fnumber <= 0)
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("cannot assign to system column \"%s\"",
+ recfield->fieldname)));
+
+ /* Cast the new value to the right type, if needed. */
+ value = exec_cast_value(estate,
+ value,
+ &isNull,
+ valtype,
+ valtypmod,
+ recfield->finfo.ftypeid,
+ recfield->finfo.ftypmod);
+
+ /* And assign it. */
+ expanded_record_set_field(erh, recfield->finfo.fnumber,
+ value, isNull);
break;
}
PLpgSQL_row *row = (PLpgSQL_row *) datum;
HeapTuple tup;
+ /* We get here if there are multiple OUT parameters */
if (!row->rowtupdesc) /* should not happen */
elog(ERROR, "row variable has no tupdesc");
/* Make sure we have a valid type/typmod setting */
{
PLpgSQL_rec *rec = (PLpgSQL_rec *) datum;
- if (!HeapTupleIsValid(rec->tup))
- ereport(ERROR,
- (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
- errmsg("record \"%s\" is not assigned yet",
- rec->refname),
- errdetail("The tuple structure of a not-yet-assigned record is indeterminate.")));
- Assert(rec->tupdesc != NULL);
- /* Make sure we have a valid type/typmod setting */
- BlessTupleDesc(rec->tupdesc);
-
- oldcontext = MemoryContextSwitchTo(get_eval_mcontext(estate));
- *typeid = rec->tupdesc->tdtypeid;
- *typetypmod = rec->tupdesc->tdtypmod;
- *value = heap_copy_tuple_as_datum(rec->tup, rec->tupdesc);
- *isnull = false;
- MemoryContextSwitchTo(oldcontext);
+ if (rec->erh == NULL)
+ {
+ /* Treat uninstantiated record as a simple NULL */
+ *value = (Datum) 0;
+ *isnull = true;
+ /* Report variable's declared type */
+ *typeid = rec->rectypeid;
+ *typetypmod = -1;
+ }
+ else
+ {
+ if (ExpandedRecordIsEmpty(rec->erh))
+ {
+ /* Empty record is also a NULL */
+ *value = (Datum) 0;
+ *isnull = true;
+ }
+ else
+ {
+ *value = ExpandedRecordGetDatum(rec->erh);
+ *isnull = false;
+ }
+ if (rec->rectypeid != RECORDOID)
+ {
+ /* Report variable's declared type, if not RECORD */
+ *typeid = rec->rectypeid;
+ *typetypmod = -1;
+ }
+ else
+ {
+ /* Report record's actual type if declared RECORD */
+ *typeid = rec->erh->er_typeid;
+ *typetypmod = rec->erh->er_typmod;
+ }
+ }
break;
}
{
PLpgSQL_recfield *recfield = (PLpgSQL_recfield *) datum;
PLpgSQL_rec *rec;
- int fno;
+ ExpandedRecordHeader *erh;
rec = (PLpgSQL_rec *) (estate->datums[recfield->recparentno]);
- if (!HeapTupleIsValid(rec->tup))
- ereport(ERROR,
- (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
- errmsg("record \"%s\" is not assigned yet",
- rec->refname),
- errdetail("The tuple structure of a not-yet-assigned record is indeterminate.")));
- fno = SPI_fnumber(rec->tupdesc, recfield->fieldname);
- if (fno == SPI_ERROR_NOATTRIBUTE)
- ereport(ERROR,
- (errcode(ERRCODE_UNDEFINED_COLUMN),
- errmsg("record \"%s\" has no field \"%s\"",
- rec->refname, recfield->fieldname)));
- *typeid = SPI_gettypeid(rec->tupdesc, fno);
- if (fno > 0)
+ erh = rec->erh;
+
+ /*
+ * If record variable is NULL, instantiate it if it has a
+ * named composite type, else complain. (This won't change
+ * the logical state of the record: it's still NULL.)
+ */
+ if (erh == NULL)
{
- Form_pg_attribute attr = TupleDescAttr(rec->tupdesc, fno - 1);
+ instantiate_empty_record_variable(estate, rec);
+ erh = rec->erh;
+ }
- *typetypmod = attr->atttypmod;
+ /*
+ * Look up the field's properties if we have not already, or
+ * if the tuple descriptor ID changed since last time.
+ */
+ if (unlikely(recfield->rectupledescid != erh->er_tupdesc_id))
+ {
+ if (!expanded_record_lookup_field(erh,
+ recfield->fieldname,
+ &recfield->finfo))
+ ereport(ERROR,
+ (errcode(ERRCODE_UNDEFINED_COLUMN),
+ errmsg("record \"%s\" has no field \"%s\"",
+ rec->refname, recfield->fieldname)));
+ recfield->rectupledescid = erh->er_tupdesc_id;
}
- else
- *typetypmod = -1;
- *value = SPI_getbinval(rec->tup, rec->tupdesc, fno, isnull);
+
+ /* Report type data. */
+ *typeid = recfield->finfo.ftypeid;
+ *typetypmod = recfield->finfo.ftypmod;
+
+ /* And fetch the field value. */
+ *value = expanded_record_get_field(erh,
+ recfield->finfo.fnumber,
+ isnull);
break;
}
/*
* plpgsql_exec_get_datum_type Get datatype of a PLpgSQL_datum
*
- * This is the same logic as in exec_eval_datum, except that it can handle
- * some cases where exec_eval_datum has to fail; specifically, we may have
- * a tupdesc but no row value for a record variable. (This currently can
- * happen only for a trigger's NEW/OLD records.)
+ * This is the same logic as in exec_eval_datum, but we skip acquiring
+ * the actual value of the variable. Also, needn't support DTYPE_ROW.
*/
Oid
plpgsql_exec_get_datum_type(PLpgSQL_execstate *estate,
break;
}
- case PLPGSQL_DTYPE_ROW:
- {
- PLpgSQL_row *row = (PLpgSQL_row *) datum;
-
- if (!row->rowtupdesc) /* should not happen */
- elog(ERROR, "row variable has no tupdesc");
- /* Make sure we have a valid type/typmod setting */
- BlessTupleDesc(row->rowtupdesc);
- typeid = row->rowtupdesc->tdtypeid;
- break;
- }
-
case PLPGSQL_DTYPE_REC:
{
PLpgSQL_rec *rec = (PLpgSQL_rec *) datum;
- if (rec->tupdesc == NULL)
- ereport(ERROR,
- (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
- errmsg("record \"%s\" is not assigned yet",
- rec->refname),
- errdetail("The tuple structure of a not-yet-assigned record is indeterminate.")));
- /* Make sure we have a valid type/typmod setting */
- BlessTupleDesc(rec->tupdesc);
- typeid = rec->tupdesc->tdtypeid;
+ if (rec->erh == NULL || rec->rectypeid != RECORDOID)
+ {
+ /* Report variable's declared type */
+ typeid = rec->rectypeid;
+ }
+ else
+ {
+ /* Report record's actual type if declared RECORD */
+ typeid = rec->erh->er_typeid;
+ }
break;
}
{
PLpgSQL_recfield *recfield = (PLpgSQL_recfield *) datum;
PLpgSQL_rec *rec;
- int fno;
rec = (PLpgSQL_rec *) (estate->datums[recfield->recparentno]);
- if (rec->tupdesc == NULL)
- ereport(ERROR,
- (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
- errmsg("record \"%s\" is not assigned yet",
- rec->refname),
- errdetail("The tuple structure of a not-yet-assigned record is indeterminate.")));
- fno = SPI_fnumber(rec->tupdesc, recfield->fieldname);
- if (fno == SPI_ERROR_NOATTRIBUTE)
- ereport(ERROR,
- (errcode(ERRCODE_UNDEFINED_COLUMN),
- errmsg("record \"%s\" has no field \"%s\"",
- rec->refname, recfield->fieldname)));
- typeid = SPI_gettypeid(rec->tupdesc, fno);
+
+ /*
+ * If record variable is NULL, instantiate it if it has a
+ * named composite type, else complain. (This won't change
+ * the logical state of the record: it's still NULL.)
+ */
+ if (rec->erh == NULL)
+ instantiate_empty_record_variable(estate, rec);
+
+ /*
+ * Look up the field's properties if we have not already, or
+ * if the tuple descriptor ID changed since last time.
+ */
+ if (unlikely(recfield->rectupledescid != rec->erh->er_tupdesc_id))
+ {
+ if (!expanded_record_lookup_field(rec->erh,
+ recfield->fieldname,
+ &recfield->finfo))
+ ereport(ERROR,
+ (errcode(ERRCODE_UNDEFINED_COLUMN),
+ errmsg("record \"%s\" has no field \"%s\"",
+ rec->refname, recfield->fieldname)));
+ recfield->rectupledescid = rec->erh->er_tupdesc_id;
+ }
+
+ typeid = recfield->finfo.ftypeid;
break;
}
* plpgsql_exec_get_datum_type_info Get datatype etc of a PLpgSQL_datum
*
* An extended version of plpgsql_exec_get_datum_type, which also retrieves the
- * typmod and collation of the datum.
+ * typmod and collation of the datum. Note however that we don't report the
+ * possibly-mutable typmod of RECORD values, but say -1 always.
*/
void
plpgsql_exec_get_datum_type_info(PLpgSQL_execstate *estate,
break;
}
- case PLPGSQL_DTYPE_ROW:
- {
- PLpgSQL_row *row = (PLpgSQL_row *) datum;
-
- if (!row->rowtupdesc) /* should not happen */
- elog(ERROR, "row variable has no tupdesc");
- /* Make sure we have a valid type/typmod setting */
- BlessTupleDesc(row->rowtupdesc);
- *typeid = row->rowtupdesc->tdtypeid;
- /* do NOT return the mutable typmod of a RECORD variable */
- *typmod = -1;
- /* composite types are never collatable */
- *collation = InvalidOid;
- break;
- }
-
case PLPGSQL_DTYPE_REC:
{
PLpgSQL_rec *rec = (PLpgSQL_rec *) datum;
- if (rec->tupdesc == NULL)
- ereport(ERROR,
- (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
- errmsg("record \"%s\" is not assigned yet",
- rec->refname),
- errdetail("The tuple structure of a not-yet-assigned record is indeterminate.")));
- /* Make sure we have a valid type/typmod setting */
- BlessTupleDesc(rec->tupdesc);
- *typeid = rec->tupdesc->tdtypeid;
- /* do NOT return the mutable typmod of a RECORD variable */
- *typmod = -1;
+ if (rec->erh == NULL || rec->rectypeid != RECORDOID)
+ {
+ /* Report variable's declared type */
+ *typeid = rec->rectypeid;
+ *typmod = -1;
+ }
+ else
+ {
+ /* Report record's actual type if declared RECORD */
+ *typeid = rec->erh->er_typeid;
+ /* do NOT return the mutable typmod of a RECORD variable */
+ *typmod = -1;
+ }
/* composite types are never collatable */
*collation = InvalidOid;
break;
{
PLpgSQL_recfield *recfield = (PLpgSQL_recfield *) datum;
PLpgSQL_rec *rec;
- int fno;
rec = (PLpgSQL_rec *) (estate->datums[recfield->recparentno]);
- if (rec->tupdesc == NULL)
- ereport(ERROR,
- (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
- errmsg("record \"%s\" is not assigned yet",
- rec->refname),
- errdetail("The tuple structure of a not-yet-assigned record is indeterminate.")));
- fno = SPI_fnumber(rec->tupdesc, recfield->fieldname);
- if (fno == SPI_ERROR_NOATTRIBUTE)
- ereport(ERROR,
- (errcode(ERRCODE_UNDEFINED_COLUMN),
- errmsg("record \"%s\" has no field \"%s\"",
- rec->refname, recfield->fieldname)));
- *typeid = SPI_gettypeid(rec->tupdesc, fno);
- if (fno > 0)
- {
- Form_pg_attribute attr = TupleDescAttr(rec->tupdesc, fno - 1);
- *typmod = attr->atttypmod;
- }
- else
- *typmod = -1;
- if (fno > 0)
- {
- Form_pg_attribute attr = TupleDescAttr(rec->tupdesc, fno - 1);
+ /*
+ * If record variable is NULL, instantiate it if it has a
+ * named composite type, else complain. (This won't change
+ * the logical state of the record: it's still NULL.)
+ */
+ if (rec->erh == NULL)
+ instantiate_empty_record_variable(estate, rec);
- *collation = attr->attcollation;
+ /*
+ * Look up the field's properties if we have not already, or
+ * if the tuple descriptor ID changed since last time.
+ */
+ if (unlikely(recfield->rectupledescid != rec->erh->er_tupdesc_id))
+ {
+ if (!expanded_record_lookup_field(rec->erh,
+ recfield->fieldname,
+ &recfield->finfo))
+ ereport(ERROR,
+ (errcode(ERRCODE_UNDEFINED_COLUMN),
+ errmsg("record \"%s\" has no field \"%s\"",
+ rec->refname, recfield->fieldname)));
+ recfield->rectupledescid = rec->erh->er_tupdesc_id;
}
- else /* no system column types have collation */
- *collation = InvalidOid;
+
+ *typeid = recfield->finfo.ftypeid;
+ *typmod = recfield->finfo.ftypmod;
+ *collation = recfield->finfo.fcollation;
break;
}
SPITupleTable *tuptab;
bool found = false;
int rc = PLPGSQL_RC_OK;
+ uint64 previous_id = INVALID_TUPLEDESC_IDENTIFIER;
+ bool tupdescs_match = true;
uint64 n;
/* Fetch loop variable's datum entry */
for (i = 0; i < n; i++)
{
/*
- * Assign the tuple to the target
+ * Assign the tuple to the target. Here, because we know that all
+ * loop iterations should be assigning the same tupdesc, we can
+ * optimize away repeated creations of expanded records with
+ * identical tupdescs. Testing for changes of er_tupdesc_id is
+ * reliable even if the loop body contains assignments that
+ * replace the target's value entirely, because it's assigned from
+ * a process-global counter. The case where the tupdescs don't
+ * match could possibly be handled more efficiently than this
+ * coding does, but it's not clear extra effort is worthwhile.
*/
- exec_move_row(estate, var, tuptab->vals[i], tuptab->tupdesc);
- exec_eval_cleanup(estate);
+ if (var->dtype == PLPGSQL_DTYPE_REC)
+ {
+ PLpgSQL_rec *rec = (PLpgSQL_rec *) var;
+
+ if (rec->erh &&
+ rec->erh->er_tupdesc_id == previous_id &&
+ tupdescs_match)
+ {
+ /* Only need to assign a new tuple value */
+ expanded_record_set_tuple(rec->erh, tuptab->vals[i], true);
+ }
+ else
+ {
+ /*
+ * First time through, or var's tupdesc changed in loop,
+ * or we have to do it the hard way because type coercion
+ * is needed.
+ */
+ exec_move_row(estate, var,
+ tuptab->vals[i], tuptab->tupdesc);
+
+ /*
+ * Check to see if physical assignment is OK next time.
+ * Once the tupdesc comparison has failed once, we don't
+ * bother rechecking in subsequent loop iterations.
+ */
+ if (tupdescs_match)
+ {
+ tupdescs_match =
+ (rec->rectypeid == RECORDOID ||
+ rec->rectypeid == tuptab->tupdesc->tdtypeid ||
+ compatible_tupdescs(tuptab->tupdesc,
+ expanded_record_get_tupdesc(rec->erh)));
+ }
+ previous_id = rec->erh->er_tupdesc_id;
+ }
+ }
+ else
+ exec_move_row(estate, var, tuptab->vals[i], tuptab->tupdesc);
+
+ exec_eval_cleanup(estate);
/*
* Execute the statements
break;
case PLPGSQL_DTYPE_REC:
- {
- PLpgSQL_rec *rec = (PLpgSQL_rec *) datum;
-
- if (!HeapTupleIsValid(rec->tup))
- ok = false;
- break;
- }
+ /* always safe (might return NULL, that's fine) */
+ break;
case PLPGSQL_DTYPE_RECFIELD:
{
PLpgSQL_recfield *recfield = (PLpgSQL_recfield *) datum;
PLpgSQL_rec *rec;
- int fno;
rec = (PLpgSQL_rec *) (estate->datums[recfield->recparentno]);
- if (!HeapTupleIsValid(rec->tup))
+
+ /*
+ * If record variable is NULL, don't risk anything.
+ */
+ if (rec->erh == NULL)
ok = false;
- else
+
+ /*
+ * Look up the field's properties if we have not already,
+ * or if the tuple descriptor ID changed since last time.
+ */
+ else if (unlikely(recfield->rectupledescid != rec->erh->er_tupdesc_id))
{
- fno = SPI_fnumber(rec->tupdesc, recfield->fieldname);
- if (fno == SPI_ERROR_NOATTRIBUTE)
+ if (expanded_record_lookup_field(rec->erh,
+ recfield->fieldname,
+ &recfield->finfo))
+ recfield->rectupledescid = rec->erh->er_tupdesc_id;
+ else
ok = false;
}
break;
* If it's a read/write expanded datum, convert reference to read-only,
* unless it's safe to pass as read-write.
*/
- if (datum->dtype == PLPGSQL_DTYPE_VAR && dno != expr->rwparam)
- prm->value = MakeExpandedObjectReadOnly(prm->value,
- prm->isnull,
- ((PLpgSQL_var *) datum)->datatype->typlen);
+ if (dno != expr->rwparam)
+ {
+ if (datum->dtype == PLPGSQL_DTYPE_VAR)
+ prm->value = MakeExpandedObjectReadOnly(prm->value,
+ prm->isnull,
+ ((PLpgSQL_var *) datum)->datatype->typlen);
+ else if (datum->dtype == PLPGSQL_DTYPE_REC)
+ prm->value = MakeExpandedObjectReadOnly(prm->value,
+ prm->isnull,
+ -1);
+ }
return prm;
}
scratch.resvalue = resv;
scratch.resnull = resnull;
- /* Select appropriate eval function */
+ /*
+ * Select appropriate eval function. It seems worth special-casing
+ * DTYPE_VAR and DTYPE_RECFIELD for performance. Also, we can determine
+ * in advance whether MakeExpandedObjectReadOnly() will be required.
+ * Currently, only VAR and REC datums could contain read/write expanded
+ * objects.
+ */
if (datum->dtype == PLPGSQL_DTYPE_VAR)
{
if (dno != expr->rwparam &&
else
scratch.d.cparam.paramfunc = plpgsql_param_eval_var;
}
+ else if (datum->dtype == PLPGSQL_DTYPE_RECFIELD)
+ scratch.d.cparam.paramfunc = plpgsql_param_eval_recfield;
+ else if (datum->dtype == PLPGSQL_DTYPE_REC &&
+ dno != expr->rwparam)
+ scratch.d.cparam.paramfunc = plpgsql_param_eval_generic_ro;
else
- scratch.d.cparam.paramfunc = plpgsql_param_eval_non_var;
+ scratch.d.cparam.paramfunc = plpgsql_param_eval_generic;
/*
* Note: it's tempting to use paramarg to store the estate pointer and
}
/*
- * plpgsql_param_eval_non_var evaluation of EEOP_PARAM_CALLBACK step
+ * plpgsql_param_eval_recfield evaluation of EEOP_PARAM_CALLBACK step
*
- * This handles all variable types except DTYPE_VAR.
+ * This is specialized to the case of DTYPE_RECFIELD variables, for which
+ * we never need to invoke MakeExpandedObjectReadOnly.
*/
static void
-plpgsql_param_eval_non_var(ExprState *state, ExprEvalStep *op,
+plpgsql_param_eval_recfield(ExprState *state, ExprEvalStep *op,
+ ExprContext *econtext)
+{
+ ParamListInfo params;
+ PLpgSQL_execstate *estate;
+ int dno = op->d.cparam.paramid - 1;
+ PLpgSQL_recfield *recfield;
+ PLpgSQL_rec *rec;
+ ExpandedRecordHeader *erh;
+
+ /* fetch back the hook data */
+ params = econtext->ecxt_param_list_info;
+ estate = (PLpgSQL_execstate *) params->paramFetchArg;
+ Assert(dno >= 0 && dno < estate->ndatums);
+
+ /* now we can access the target datum */
+ recfield = (PLpgSQL_recfield *) estate->datums[dno];
+ Assert(recfield->dtype == PLPGSQL_DTYPE_RECFIELD);
+
+ /* inline the relevant part of exec_eval_datum */
+ rec = (PLpgSQL_rec *) (estate->datums[recfield->recparentno]);
+ erh = rec->erh;
+
+ /*
+ * If record variable is NULL, instantiate it if it has a named composite
+ * type, else complain. (This won't change the logical state of the
+ * record: it's still NULL.)
+ */
+ if (erh == NULL)
+ {
+ instantiate_empty_record_variable(estate, rec);
+ erh = rec->erh;
+ }
+
+ /*
+ * Look up the field's properties if we have not already, or if the tuple
+ * descriptor ID changed since last time.
+ */
+ if (unlikely(recfield->rectupledescid != erh->er_tupdesc_id))
+ {
+ if (!expanded_record_lookup_field(erh,
+ recfield->fieldname,
+ &recfield->finfo))
+ ereport(ERROR,
+ (errcode(ERRCODE_UNDEFINED_COLUMN),
+ errmsg("record \"%s\" has no field \"%s\"",
+ rec->refname, recfield->fieldname)));
+ recfield->rectupledescid = erh->er_tupdesc_id;
+ }
+
+ /* OK to fetch the field value. */
+ *op->resvalue = expanded_record_get_field(erh,
+ recfield->finfo.fnumber,
+ op->resnull);
+
+ /* safety check -- needed for, eg, record fields */
+ if (unlikely(recfield->finfo.ftypeid != op->d.cparam.paramtype))
+ ereport(ERROR,
+ (errcode(ERRCODE_DATATYPE_MISMATCH),
+ errmsg("type of parameter %d (%s) does not match that when preparing the plan (%s)",
+ op->d.cparam.paramid,
+ format_type_be(recfield->finfo.ftypeid),
+ format_type_be(op->d.cparam.paramtype))));
+}
+
+/*
+ * plpgsql_param_eval_generic evaluation of EEOP_PARAM_CALLBACK step
+ *
+ * This handles all variable types, but assumes we do not need to invoke
+ * MakeExpandedObjectReadOnly.
+ */
+static void
+plpgsql_param_eval_generic(ExprState *state, ExprEvalStep *op,
ExprContext *econtext)
{
ParamListInfo params;
/* now we can access the target datum */
datum = estate->datums[dno];
- Assert(datum->dtype != PLPGSQL_DTYPE_VAR);
+ /* fetch datum's value */
exec_eval_datum(estate, datum,
&datumtype, &datumtypmod,
op->resvalue, op->resnull);
op->d.cparam.paramid,
format_type_be(datumtype),
format_type_be(op->d.cparam.paramtype))));
+}
- /*
- * Currently, if the dtype isn't VAR, the value couldn't be a read/write
- * expanded datum.
- */
+/*
+ * plpgsql_param_eval_generic_ro evaluation of EEOP_PARAM_CALLBACK step
+ *
+ * This handles all variable types, but assumes we need to invoke
+ * MakeExpandedObjectReadOnly (hence, variable must be of a varlena type).
+ */
+static void
+plpgsql_param_eval_generic_ro(ExprState *state, ExprEvalStep *op,
+ ExprContext *econtext)
+{
+ ParamListInfo params;
+ PLpgSQL_execstate *estate;
+ int dno = op->d.cparam.paramid - 1;
+ PLpgSQL_datum *datum;
+ Oid datumtype;
+ int32 datumtypmod;
+
+ /* fetch back the hook data */
+ params = econtext->ecxt_param_list_info;
+ estate = (PLpgSQL_execstate *) params->paramFetchArg;
+ Assert(dno >= 0 && dno < estate->ndatums);
+
+ /* now we can access the target datum */
+ datum = estate->datums[dno];
+
+ /* fetch datum's value */
+ exec_eval_datum(estate, datum,
+ &datumtype, &datumtypmod,
+ op->resvalue, op->resnull);
+
+ /* safety check -- needed for, eg, record fields */
+ if (unlikely(datumtype != op->d.cparam.paramtype))
+ ereport(ERROR,
+ (errcode(ERRCODE_DATATYPE_MISMATCH),
+ errmsg("type of parameter %d (%s) does not match that when preparing the plan (%s)",
+ op->d.cparam.paramid,
+ format_type_be(datumtype),
+ format_type_be(op->d.cparam.paramtype))));
+
+ /* force the value to read-only */
+ *op->resvalue = MakeExpandedObjectReadOnly(*op->resvalue,
+ *op->resnull,
+ -1);
}
-/* ----------
+/*
* exec_move_row Move one tuple's values into a record or row
*
- * Since this uses exec_assign_value, caller should eventually call
+ * tup and tupdesc may both be NULL if we're just assigning an indeterminate
+ * composite NULL to the target. Alternatively, can have tup be NULL and
+ * tupdesc not NULL, in which case we assign a row of NULLs to the target.
+ *
+ * Since this uses the mcontext for workspace, caller should eventually call
* exec_eval_cleanup to prevent long-term memory leaks.
- * ----------
*/
static void
exec_move_row(PLpgSQL_execstate *estate,
PLpgSQL_variable *target,
HeapTuple tup, TupleDesc tupdesc)
{
+ ExpandedRecordHeader *newerh = NULL;
+
/*
- * Record is simple - just copy the tuple and its descriptor into the
- * record variable
+ * If target is RECORD, we may be able to avoid field-by-field processing.
*/
if (target->dtype == PLPGSQL_DTYPE_REC)
{
PLpgSQL_rec *rec = (PLpgSQL_rec *) target;
/*
- * Copy input first, just in case it is pointing at variable's value
+ * If we have no source tupdesc, just set the record variable to NULL.
+ * (If we have a source tupdesc but not a tuple, we'll set the
+ * variable to a row of nulls, instead. This is odd perhaps, but
+ * backwards compatible.)
+ */
+ if (tupdesc == NULL)
+ {
+ if (rec->erh)
+ DeleteExpandedObject(ExpandedRecordGetDatum(rec->erh));
+ rec->erh = NULL;
+ return;
+ }
+
+ /*
+ * Build a new expanded record with appropriate tupdesc.
+ */
+ newerh = make_expanded_record_for_rec(estate, rec, tupdesc, NULL);
+
+ /*
+ * If the rowtypes match, or if we have no tuple anyway, we can
+ * complete the assignment without field-by-field processing.
+ *
+ * The tests here are ordered more or less in order of cheapness. We
+ * can easily detect it will work if the target is declared RECORD or
+ * has the same typeid as the source. But when assigning from a query
+ * result, it's common to have a source tupdesc that's labeled RECORD
+ * but is actually physically compatible with a named-composite-type
+ * target, so it's worth spending extra cycles to check for that.
*/
- if (HeapTupleIsValid(tup))
- tup = heap_copytuple(tup);
- else if (tupdesc)
+ if (rec->rectypeid == RECORDOID ||
+ rec->rectypeid == tupdesc->tdtypeid ||
+ !HeapTupleIsValid(tup) ||
+ compatible_tupdescs(tupdesc, expanded_record_get_tupdesc(newerh)))
{
- /* If we have a tupdesc but no data, form an all-nulls tuple */
- bool *nulls;
+ if (!HeapTupleIsValid(tup))
+ {
+ /* No data, so force the record into all-nulls state */
+ deconstruct_expanded_record(newerh);
+ }
+ else
+ {
+ /* No coercion is needed, so just assign the row value */
+ expanded_record_set_tuple(newerh, tup, true);
+ }
- nulls = (bool *)
- eval_mcontext_alloc(estate, tupdesc->natts * sizeof(bool));
- memset(nulls, true, tupdesc->natts * sizeof(bool));
+ /* Complete the assignment */
+ assign_record_var(estate, rec, newerh);
- tup = heap_form_tuple(tupdesc, NULL, nulls);
+ return;
}
+ }
- if (tupdesc)
- tupdesc = CreateTupleDescCopy(tupdesc);
+ /*
+ * Otherwise, deconstruct the tuple and do field-by-field assignment,
+ * using exec_move_row_from_fields.
+ */
+ if (tupdesc && HeapTupleIsValid(tup))
+ {
+ int td_natts = tupdesc->natts;
+ Datum *values;
+ bool *nulls;
+ Datum values_local[64];
+ bool nulls_local[64];
- /* Free the old value ... */
- if (rec->freetup)
+ /*
+ * Need workspace arrays. If td_natts is small enough, use local
+ * arrays to save doing a palloc. Even if it's not small, we can
+ * allocate both the Datum and isnull arrays in one palloc chunk.
+ */
+ if (td_natts <= lengthof(values_local))
{
- heap_freetuple(rec->tup);
- rec->freetup = false;
+ values = values_local;
+ nulls = nulls_local;
}
- if (rec->freetupdesc)
+ else
{
- FreeTupleDesc(rec->tupdesc);
- rec->freetupdesc = false;
+ char *chunk;
+
+ chunk = eval_mcontext_alloc(estate,
+ td_natts * (sizeof(Datum) + sizeof(bool)));
+ values = (Datum *) chunk;
+ nulls = (bool *) (chunk + td_natts * sizeof(Datum));
}
- /* ... and install the new */
- if (HeapTupleIsValid(tup))
+ heap_deform_tuple(tup, tupdesc, values, nulls);
+
+ exec_move_row_from_fields(estate, target, newerh,
+ values, nulls, tupdesc);
+ }
+ else
+ {
+ /*
+ * Assign all-nulls.
+ */
+ exec_move_row_from_fields(estate, target, newerh,
+ NULL, NULL, NULL);
+ }
+}
+
+/*
+ * Build an expanded record object suitable for assignment to "rec".
+ *
+ * Caller must supply either a source tuple descriptor or a source expanded
+ * record (not both). If the record variable has declared type RECORD,
+ * it'll adopt the source's rowtype. Even if it doesn't, we may be able to
+ * piggyback on a source expanded record to save a typcache lookup.
+ *
+ * Caller must fill the object with data, then do assign_record_var().
+ *
+ * The new record is initially put into the mcontext, so it will be cleaned up
+ * if we fail before reaching assign_record_var().
+ */
+static ExpandedRecordHeader *
+make_expanded_record_for_rec(PLpgSQL_execstate *estate,
+ PLpgSQL_rec *rec,
+ TupleDesc srctupdesc,
+ ExpandedRecordHeader *srcerh)
+{
+ ExpandedRecordHeader *newerh;
+ MemoryContext mcontext = get_eval_mcontext(estate);
+
+ if (rec->rectypeid != RECORDOID)
+ {
+ /*
+ * New record must be of desired type, but maybe srcerh has already
+ * done all the same lookups.
+ */
+ if (srcerh && rec->rectypeid == srcerh->er_decltypeid)
+ newerh = make_expanded_record_from_exprecord(srcerh,
+ mcontext);
+ else
+ newerh = make_expanded_record_from_typeid(rec->rectypeid, -1,
+ mcontext);
+ }
+ else
+ {
+ /*
+ * We'll adopt the input tupdesc. We can still use
+ * make_expanded_record_from_exprecord, if srcerh isn't a composite
+ * domain. (If it is, we effectively adopt its base type.)
+ */
+ if (srcerh && !ExpandedRecordIsDomain(srcerh))
+ newerh = make_expanded_record_from_exprecord(srcerh,
+ mcontext);
+ else
{
- rec->tup = tup;
- rec->freetup = true;
+ if (!srctupdesc)
+ srctupdesc = expanded_record_get_tupdesc(srcerh);
+ newerh = make_expanded_record_from_tupdesc(srctupdesc,
+ mcontext);
}
- else
- rec->tup = NULL;
+ }
+
+ return newerh;
+}
- if (tupdesc)
+/*
+ * exec_move_row_from_fields Move arrays of field values into a record or row
+ *
+ * When assigning to a record, the caller must have already created a suitable
+ * new expanded record object, newerh. Pass NULL when assigning to a row.
+ *
+ * tupdesc describes the input row, which might have different column
+ * types and/or different dropped-column positions than the target.
+ * values/nulls/tupdesc can all be NULL if we just want to assign nulls to
+ * all fields of the record or row.
+ *
+ * Since this uses the mcontext for workspace, caller should eventually call
+ * exec_eval_cleanup to prevent long-term memory leaks.
+ */
+static void
+exec_move_row_from_fields(PLpgSQL_execstate *estate,
+ PLpgSQL_variable *target,
+ ExpandedRecordHeader *newerh,
+ Datum *values, bool *nulls,
+ TupleDesc tupdesc)
+{
+ int td_natts = tupdesc ? tupdesc->natts : 0;
+ int fnum;
+ int anum;
+
+ /* Handle RECORD-target case */
+ if (target->dtype == PLPGSQL_DTYPE_REC)
+ {
+ PLpgSQL_rec *rec = (PLpgSQL_rec *) target;
+ TupleDesc var_tupdesc;
+ Datum newvalues_local[64];
+ bool newnulls_local[64];
+
+ Assert(newerh != NULL); /* caller must have built new object */
+
+ var_tupdesc = expanded_record_get_tupdesc(newerh);
+
+ /*
+ * Coerce field values if needed. This might involve dealing with
+ * different sets of dropped columns and/or coercing individual column
+ * types. That's sort of a pain, but historically plpgsql has allowed
+ * it, so we preserve the behavior. However, it's worth a quick check
+ * to see if the tupdescs are identical. (Since expandedrecord.c
+ * prefers to use refcounted tupdescs from the typcache, expanded
+ * records with the same rowtype will have pointer-equal tupdescs.)
+ */
+ if (var_tupdesc != tupdesc)
{
- rec->tupdesc = tupdesc;
- rec->freetupdesc = true;
+ int vtd_natts = var_tupdesc->natts;
+ Datum *newvalues;
+ bool *newnulls;
+
+ /*
+ * Need workspace arrays. If vtd_natts is small enough, use local
+ * arrays to save doing a palloc. Even if it's not small, we can
+ * allocate both the Datum and isnull arrays in one palloc chunk.
+ */
+ if (vtd_natts <= lengthof(newvalues_local))
+ {
+ newvalues = newvalues_local;
+ newnulls = newnulls_local;
+ }
+ else
+ {
+ char *chunk;
+
+ chunk = eval_mcontext_alloc(estate,
+ vtd_natts * (sizeof(Datum) + sizeof(bool)));
+ newvalues = (Datum *) chunk;
+ newnulls = (bool *) (chunk + vtd_natts * sizeof(Datum));
+ }
+
+ /* Walk over destination columns */
+ anum = 0;
+ for (fnum = 0; fnum < vtd_natts; fnum++)
+ {
+ Form_pg_attribute attr = TupleDescAttr(var_tupdesc, fnum);
+ Datum value;
+ bool isnull;
+ Oid valtype;
+ int32 valtypmod;
+
+ if (attr->attisdropped)
+ {
+ /* expanded_record_set_fields should ignore this column */
+ continue; /* skip dropped column in record */
+ }
+
+ while (anum < td_natts &&
+ TupleDescAttr(tupdesc, anum)->attisdropped)
+ anum++; /* skip dropped column in tuple */
+
+ if (anum < td_natts)
+ {
+ value = values[anum];
+ isnull = nulls[anum];
+ valtype = TupleDescAttr(tupdesc, anum)->atttypid;
+ valtypmod = TupleDescAttr(tupdesc, anum)->atttypmod;
+ anum++;
+ }
+ else
+ {
+ value = (Datum) 0;
+ isnull = true;
+ valtype = UNKNOWNOID;
+ valtypmod = -1;
+ }
+
+ /* Cast the new value to the right type, if needed. */
+ newvalues[fnum] = exec_cast_value(estate,
+ value,
+ &isnull,
+ valtype,
+ valtypmod,
+ attr->atttypid,
+ attr->atttypmod);
+ newnulls[fnum] = isnull;
+ }
+
+ values = newvalues;
+ nulls = newnulls;
}
- else
- rec->tupdesc = NULL;
+
+ /* Insert the coerced field values into the new expanded record */
+ expanded_record_set_fields(newerh, values, nulls);
+
+ /* Complete the assignment */
+ assign_record_var(estate, rec, newerh);
return;
}
+ /* newerh should not have been passed in non-RECORD cases */
+ Assert(newerh == NULL);
+
/*
- * Row is a bit more complicated in that we assign the individual
- * attributes of the tuple to the variables the row points to.
+ * For a row, we assign the individual field values to the variables the
+ * row points to.
*
- * NOTE: this code used to demand row->nfields ==
- * HeapTupleHeaderGetNatts(tup->t_data), but that's wrong. The tuple
- * might have more fields than we expected if it's from an
- * inheritance-child table of the current table, or it might have fewer if
- * the table has had columns added by ALTER TABLE. Ignore extra columns
- * and assume NULL for missing columns, the same as heap_getattr would do.
- * We also have to skip over dropped columns in either the source or
- * destination.
+ * NOTE: both this code and the record code above silently ignore extra
+ * columns in the source and assume NULL for missing columns. This is
+ * pretty dubious but it's the historical behavior.
*
- * If we have no tuple data at all, we'll assign NULL to all columns of
+ * If we have no input data at all, we'll assign NULL to all columns of
* the row variable.
*/
if (target->dtype == PLPGSQL_DTYPE_ROW)
{
PLpgSQL_row *row = (PLpgSQL_row *) target;
- int td_natts = tupdesc ? tupdesc->natts : 0;
- int t_natts;
- int fnum;
- int anum;
-
- if (HeapTupleIsValid(tup))
- t_natts = HeapTupleHeaderGetNatts(tup->t_data);
- else
- t_natts = 0;
anum = 0;
for (fnum = 0; fnum < row->nfields; fnum++)
Oid valtype;
int32 valtypmod;
- if (row->varnos[fnum] < 0)
- continue; /* skip dropped column in row struct */
-
var = (PLpgSQL_var *) (estate->datums[row->varnos[fnum]]);
while (anum < td_natts &&
if (anum < td_natts)
{
- if (anum < t_natts)
- value = SPI_getbinval(tup, tupdesc, anum + 1, &isnull);
- else
- {
- value = (Datum) 0;
- isnull = true;
- }
+ value = values[anum];
+ isnull = nulls[anum];
valtype = TupleDescAttr(tupdesc, anum)->atttypid;
valtypmod = TupleDescAttr(tupdesc, anum)->atttypmod;
anum++;
elog(ERROR, "unsupported target");
}
+/*
+ * compatible_tupdescs: detect whether two tupdescs are physically compatible
+ *
+ * TRUE indicates that a tuple satisfying src_tupdesc can be used directly as
+ * a value for a composite variable using dst_tupdesc.
+ */
+static bool
+compatible_tupdescs(TupleDesc src_tupdesc, TupleDesc dst_tupdesc)
+{
+ int i;
+
+ /* Possibly we could allow src_tupdesc to have extra columns? */
+ if (dst_tupdesc->natts != src_tupdesc->natts)
+ return false;
+
+ for (i = 0; i < dst_tupdesc->natts; i++)
+ {
+ Form_pg_attribute dattr = TupleDescAttr(dst_tupdesc, i);
+ Form_pg_attribute sattr = TupleDescAttr(src_tupdesc, i);
+
+ if (dattr->attisdropped != sattr->attisdropped)
+ return false;
+ if (!dattr->attisdropped)
+ {
+ /* Normal columns must match by type and typmod */
+ if (dattr->atttypid != sattr->atttypid ||
+ (dattr->atttypmod >= 0 &&
+ dattr->atttypmod != sattr->atttypmod))
+ return false;
+ }
+ else
+ {
+ /* Dropped columns are OK as long as length/alignment match */
+ if (dattr->attlen != sattr->attlen ||
+ dattr->attalign != sattr->attalign)
+ return false;
+ }
+ }
+ return true;
+}
+
/* ----------
* make_tuple_from_row Make a tuple from the values of a row object
*
nulls[i] = true; /* leave the column as null */
continue;
}
- if (row->varnos[i] < 0) /* should not happen */
- elog(ERROR, "dropped rowtype entry for non-dropped column");
exec_eval_datum(estate, estate->datums[row->varnos[i]],
&fieldtypeid, &fieldtypmod,
return tuple;
}
-/* ----------
- * get_tuple_from_datum extract a tuple from a composite Datum
+/*
+ * deconstruct_composite_datum extract tuple+tupdesc from composite Datum
*
- * Returns a HeapTuple, freshly palloc'd in caller's context.
+ * The caller must supply a HeapTupleData variable, in which we set up a
+ * tuple header pointing to the composite datum's body. To make the tuple
+ * value outlive that variable, caller would need to apply heap_copytuple...
+ * but current callers only need a short-lived tuple value anyway.
*
- * Note: it's caller's responsibility to be sure value is of composite type.
- * ----------
- */
-static HeapTuple
-get_tuple_from_datum(Datum value)
-{
- HeapTupleHeader td = DatumGetHeapTupleHeader(value);
- HeapTupleData tmptup;
-
- /* Build a temporary HeapTuple control structure */
- tmptup.t_len = HeapTupleHeaderGetDatumLength(td);
- ItemPointerSetInvalid(&(tmptup.t_self));
- tmptup.t_tableOid = InvalidOid;
- tmptup.t_data = td;
-
- /* Build a copy and return it */
- return heap_copytuple(&tmptup);
-}
-
-/* ----------
- * get_tupdesc_from_datum get a tuple descriptor for a composite Datum
- *
- * Returns a pointer to the TupleDesc of the tuple's rowtype.
+ * Returns a pointer to the TupleDesc of the datum's rowtype.
* Caller is responsible for calling ReleaseTupleDesc when done with it.
*
* Note: it's caller's responsibility to be sure value is of composite type.
- * ----------
+ * Also, best to call this in a short-lived context, as it might leak memory.
*/
static TupleDesc
-get_tupdesc_from_datum(Datum value)
+deconstruct_composite_datum(Datum value, HeapTupleData *tmptup)
{
- HeapTupleHeader td = DatumGetHeapTupleHeader(value);
+ HeapTupleHeader td;
Oid tupType;
int32 tupTypmod;
+ /* Get tuple body (note this could involve detoasting) */
+ td = DatumGetHeapTupleHeader(value);
+
+ /* Build a temporary HeapTuple control structure */
+ tmptup->t_len = HeapTupleHeaderGetDatumLength(td);
+ ItemPointerSetInvalid(&(tmptup->t_self));
+ tmptup->t_tableOid = InvalidOid;
+ tmptup->t_data = td;
+
/* Extract rowtype info and find a tupdesc */
tupType = HeapTupleHeaderGetTypeId(td);
tupTypmod = HeapTupleHeaderGetTypMod(td);
return lookup_rowtype_tupdesc(tupType, tupTypmod);
}
-/* ----------
+/*
* exec_move_row_from_datum Move a composite Datum into a record or row
*
- * This is equivalent to get_tuple_from_datum() followed by exec_move_row(),
- * but we avoid constructing an intermediate physical copy of the tuple.
- * ----------
+ * This is equivalent to deconstruct_composite_datum() followed by
+ * exec_move_row(), but we can optimize things if the Datum is an
+ * expanded-record reference.
+ *
+ * Note: it's caller's responsibility to be sure value is of composite type.
*/
static void
exec_move_row_from_datum(PLpgSQL_execstate *estate,
PLpgSQL_variable *target,
Datum value)
{
- HeapTupleHeader td = DatumGetHeapTupleHeader(value);
- Oid tupType;
- int32 tupTypmod;
- TupleDesc tupdesc;
- HeapTupleData tmptup;
+ /* Check to see if source is an expanded record */
+ if (VARATT_IS_EXTERNAL_EXPANDED(DatumGetPointer(value)))
+ {
+ ExpandedRecordHeader *erh = (ExpandedRecordHeader *) DatumGetEOHP(value);
+ ExpandedRecordHeader *newerh = NULL;
- /* Extract rowtype info and find a tupdesc */
- tupType = HeapTupleHeaderGetTypeId(td);
- tupTypmod = HeapTupleHeaderGetTypMod(td);
- tupdesc = lookup_rowtype_tupdesc(tupType, tupTypmod);
+ Assert(erh->er_magic == ER_MAGIC);
- /* Build a temporary HeapTuple control structure */
- tmptup.t_len = HeapTupleHeaderGetDatumLength(td);
- ItemPointerSetInvalid(&(tmptup.t_self));
- tmptup.t_tableOid = InvalidOid;
- tmptup.t_data = td;
+ /* These cases apply if the target is record not row... */
+ if (target->dtype == PLPGSQL_DTYPE_REC)
+ {
+ PLpgSQL_rec *rec = (PLpgSQL_rec *) target;
+
+ /*
+ * If it's the same record already stored in the variable, do
+ * nothing. This would happen only in silly cases like "r := r",
+ * but we need some check to avoid possibly freeing the variable's
+ * live value below. Note that this applies even if what we have
+ * is a R/O pointer.
+ */
+ if (erh == rec->erh)
+ return;
+
+ /*
+ * If we have a R/W pointer, we're allowed to just commandeer
+ * ownership of the expanded record. If it's of the right type to
+ * put into the record variable, do that. (Note we don't accept
+ * an expanded record of a composite-domain type as a RECORD
+ * value. We'll treat it as the base composite type instead;
+ * compare logic in make_expanded_record_for_rec.)
+ */
+ if (VARATT_IS_EXTERNAL_EXPANDED_RW(DatumGetPointer(value)) &&
+ (rec->rectypeid == erh->er_decltypeid ||
+ (rec->rectypeid == RECORDOID &&
+ !ExpandedRecordIsDomain(erh))))
+ {
+ assign_record_var(estate, rec, erh);
+ return;
+ }
+
+ /*
+ * If we already have an expanded record object in the target
+ * variable, and the source record contains a valid tuple
+ * representation with the right rowtype, then we can skip making
+ * a new expanded record and just assign the tuple with
+ * expanded_record_set_tuple. (We can't do the equivalent if we
+ * have to do field-by-field assignment, since that wouldn't be
+ * atomic if there's an error.) We consider that there's a
+ * rowtype match only if it's the same named composite type or
+ * same registered rowtype; checking for matches of anonymous
+ * rowtypes would be more expensive than this is worth.
+ */
+ if (rec->erh &&
+ (erh->flags & ER_FLAG_FVALUE_VALID) &&
+ erh->er_typeid == rec->erh->er_typeid &&
+ (erh->er_typeid != RECORDOID ||
+ (erh->er_typmod == rec->erh->er_typmod &&
+ erh->er_typmod >= 0)))
+ {
+ expanded_record_set_tuple(rec->erh, erh->fvalue, true);
+ return;
+ }
+
+ /*
+ * Otherwise we're gonna need a new expanded record object. Make
+ * it here in hopes of piggybacking on the source object's
+ * previous typcache lookup.
+ */
+ newerh = make_expanded_record_for_rec(estate, rec, NULL, erh);
+
+ /*
+ * If the expanded record contains a valid tuple representation,
+ * and we don't need rowtype conversion, then just copying the
+ * tuple is probably faster than field-by-field processing. (This
+ * isn't duplicative of the previous check, since here we will
+ * catch the case where the record variable was previously empty.)
+ */
+ if ((erh->flags & ER_FLAG_FVALUE_VALID) &&
+ (rec->rectypeid == RECORDOID ||
+ rec->rectypeid == erh->er_typeid))
+ {
+ expanded_record_set_tuple(newerh, erh->fvalue, true);
+ assign_record_var(estate, rec, newerh);
+ return;
+ }
+
+ /*
+ * Need to special-case empty source record, else code below would
+ * leak newerh.
+ */
+ if (ExpandedRecordIsEmpty(erh))
+ {
+ /* Set newerh to a row of NULLs */
+ deconstruct_expanded_record(newerh);
+ assign_record_var(estate, rec, newerh);
+ return;
+ }
+ } /* end of record-target-only cases */
+
+ /*
+ * If the source expanded record is empty, we should treat that like a
+ * NULL tuple value. (We're unlikely to see such a case, but we must
+ * check this; deconstruct_expanded_record would cause a change of
+ * logical state, which is not OK.)
+ */
+ if (ExpandedRecordIsEmpty(erh))
+ {
+ exec_move_row(estate, target, NULL,
+ expanded_record_get_tupdesc(erh));
+ return;
+ }
+
+ /*
+ * Otherwise, ensure that the source record is deconstructed, and
+ * assign from its field values.
+ */
+ deconstruct_expanded_record(erh);
+ exec_move_row_from_fields(estate, target, newerh,
+ erh->dvalues, erh->dnulls,
+ expanded_record_get_tupdesc(erh));
+ }
+ else
+ {
+ /*
+ * Nope, we've got a plain composite Datum. Deconstruct it; but we
+ * don't use deconstruct_composite_datum(), because we may be able to
+ * skip calling lookup_rowtype_tupdesc().
+ */
+ HeapTupleHeader td;
+ HeapTupleData tmptup;
+ Oid tupType;
+ int32 tupTypmod;
+ TupleDesc tupdesc;
+ MemoryContext oldcontext;
+
+ /* Ensure that any detoasted data winds up in the eval_mcontext */
+ oldcontext = MemoryContextSwitchTo(get_eval_mcontext(estate));
+ /* Get tuple body (note this could involve detoasting) */
+ td = DatumGetHeapTupleHeader(value);
+ MemoryContextSwitchTo(oldcontext);
+
+ /* Build a temporary HeapTuple control structure */
+ tmptup.t_len = HeapTupleHeaderGetDatumLength(td);
+ ItemPointerSetInvalid(&(tmptup.t_self));
+ tmptup.t_tableOid = InvalidOid;
+ tmptup.t_data = td;
- /* Do the move */
- exec_move_row(estate, target, &tmptup, tupdesc);
+ /* Extract rowtype info */
+ tupType = HeapTupleHeaderGetTypeId(td);
+ tupTypmod = HeapTupleHeaderGetTypMod(td);
- /* Release tupdesc usage count */
- ReleaseTupleDesc(tupdesc);
+ /* Now, if the target is record not row, maybe we can optimize ... */
+ if (target->dtype == PLPGSQL_DTYPE_REC)
+ {
+ PLpgSQL_rec *rec = (PLpgSQL_rec *) target;
+
+ /*
+ * If we already have an expanded record object in the target
+ * variable, and the source datum has a matching rowtype, then we
+ * can skip making a new expanded record and just assign the tuple
+ * with expanded_record_set_tuple. We consider that there's a
+ * rowtype match only if it's the same named composite type or
+ * same registered rowtype. (Checking to reject an anonymous
+ * rowtype here should be redundant, but let's be safe.)
+ */
+ if (rec->erh &&
+ tupType == rec->erh->er_typeid &&
+ (tupType != RECORDOID ||
+ (tupTypmod == rec->erh->er_typmod &&
+ tupTypmod >= 0)))
+ {
+ expanded_record_set_tuple(rec->erh, &tmptup, true);
+ return;
+ }
+
+ /*
+ * If the source datum has a rowtype compatible with the target
+ * variable, just build a new expanded record and assign the tuple
+ * into it. Using make_expanded_record_from_typeid() here saves
+ * one typcache lookup compared to the code below.
+ */
+ if (rec->rectypeid == RECORDOID || rec->rectypeid == tupType)
+ {
+ ExpandedRecordHeader *newerh;
+ MemoryContext mcontext = get_eval_mcontext(estate);
+
+ newerh = make_expanded_record_from_typeid(tupType, tupTypmod,
+ mcontext);
+ expanded_record_set_tuple(newerh, &tmptup, true);
+ assign_record_var(estate, rec, newerh);
+ return;
+ }
+
+ /*
+ * Otherwise, we're going to need conversion, so fall through to
+ * do it the hard way.
+ */
+ }
+
+ /*
+ * ROW target, or unoptimizable RECORD target, so we have to expend a
+ * lookup to obtain the source datum's tupdesc.
+ */
+ tupdesc = lookup_rowtype_tupdesc(tupType, tupTypmod);
+
+ /* Do the move */
+ exec_move_row(estate, target, &tmptup, tupdesc);
+
+ /* Release tupdesc usage count */
+ ReleaseTupleDesc(tupdesc);
+ }
+}
+
+/*
+ * If we have not created an expanded record to hold the record variable's
+ * value, do so. The expanded record will be "empty", so this does not
+ * change the logical state of the record variable: it's still NULL.
+ * However, now we'll have a tupdesc with which we can e.g. look up fields.
+ */
+static void
+instantiate_empty_record_variable(PLpgSQL_execstate *estate, PLpgSQL_rec *rec)
+{
+ Assert(rec->erh == NULL); /* else caller error */
+
+ /* If declared type is RECORD, we can't instantiate */
+ if (rec->rectypeid == RECORDOID)
+ ereport(ERROR,
+ (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+ errmsg("record \"%s\" is not assigned yet", rec->refname),
+ errdetail("The tuple structure of a not-yet-assigned record is indeterminate.")));
+
+ /* OK, do it */
+ rec->erh = make_expanded_record_from_typeid(rec->rectypeid, -1,
+ estate->datum_context);
}
/* ----------
assign_simple_var(estate, var, CStringGetTextDatum(str), false, true);
}
+/*
+ * assign_record_var --- assign a new value to any REC datum.
+ */
+static void
+assign_record_var(PLpgSQL_execstate *estate, PLpgSQL_rec *rec,
+ ExpandedRecordHeader *erh)
+{
+ Assert(rec->dtype == PLPGSQL_DTYPE_REC);
+
+ /* Transfer new record object into datum_context */
+ TransferExpandedRecord(erh, estate->datum_context);
+
+ /* Free the old value ... */
+ if (rec->erh)
+ DeleteExpandedObject(ExpandedRecordGetDatum(rec->erh));
+
+ /* ... and install the new */
+ rec->erh = erh;
+}
+
/*
* exec_eval_using_params --- evaluate params of USING clause
*
printf("ROW %-16s fields", row->refname);
for (i = 0; i < row->nfields; i++)
{
- if (row->fieldnames[i])
- printf(" %s=var %d", row->fieldnames[i],
- row->varnos[i]);
+ printf(" %s=var %d", row->fieldnames[i],
+ row->varnos[i]);
}
printf("\n");
}
break;
case PLPGSQL_DTYPE_REC:
- printf("REC %s\n", ((PLpgSQL_rec *) d)->refname);
+ printf("REC %-16s typoid %u\n",
+ ((PLpgSQL_rec *) d)->refname,
+ ((PLpgSQL_rec *) d)->rectypeid);
break;
case PLPGSQL_DTYPE_RECFIELD:
printf("RECFIELD %-16s of REC %d\n",
else
ereport(ERROR,
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
- errmsg("row or record variable cannot be CONSTANT"),
+ errmsg("record variable cannot be CONSTANT"),
parser_errposition(@2)));
}
if ($5)
else
ereport(ERROR,
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
- errmsg("row or record variable cannot be NOT NULL"),
+ errmsg("record variable cannot be NOT NULL"),
parser_errposition(@4)));
}
else
ereport(ERROR,
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
- errmsg("default value for row or record variable is not supported"),
+ errmsg("default value for record variable is not supported"),
parser_errposition(@5)));
}
}
{
ereport(ERROR,
(errcode(ERRCODE_DATATYPE_MISMATCH),
- errmsg("loop variable of loop over rows must be a record or row variable or list of scalar variables"),
+ errmsg("loop variable of loop over rows must be a record variable or list of scalar variables"),
parser_errposition(@1)));
}
new->query = expr;
new->var = (PLpgSQL_variable *)
plpgsql_build_record($1.name,
$1.lineno,
+ RECORDOID,
true);
$$ = (PLpgSQL_stmt *) new;
{
ereport(ERROR,
(errcode(ERRCODE_SYNTAX_ERROR),
- errmsg("loop variable of loop over rows must be a record or row variable or list of scalar variables"),
+ errmsg("loop variable of loop over rows must be a record variable or list of scalar variables"),
parser_errposition(@1)));
}
parser_errposition(location)));
break;
case PLPGSQL_DTYPE_ROW:
- /* always assignable? */
+ /* always assignable? Shouldn't we check member vars? */
break;
case PLPGSQL_DTYPE_REC:
/* always assignable? What about NEW/OLD? */
if ((tok = yylex()) == ',')
ereport(ERROR,
(errcode(ERRCODE_SYNTAX_ERROR),
- errmsg("record or row variable cannot be part of multiple-item INTO list"),
+ errmsg("record variable cannot be part of multiple-item INTO list"),
parser_errposition(yylloc)));
plpgsql_push_back_token(tok);
}
}
/* Disallow pseudotypes in arguments (either IN or OUT) */
- /* except for polymorphic */
+ /* except for RECORD and polymorphic */
numargs = get_func_arg_info(tuple,
&argtypes, &argnames, &argmodes);
for (i = 0; i < numargs; i++)
{
if (get_typtype(argtypes[i]) == TYPTYPE_PSEUDO)
{
- if (!IsPolymorphicType(argtypes[i]))
+ if (argtypes[i] != RECORDOID &&
+ !IsPolymorphicType(argtypes[i]))
ereport(ERROR,
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
errmsg("PL/pgSQL functions cannot accept type %s",
#include "commands/event_trigger.h"
#include "commands/trigger.h"
#include "executor/spi.h"
+#include "utils/expandedrecord.h"
+
/**********************************************************************
* Definitions
*/
typedef enum PLpgSQL_nsitem_type
{
- PLPGSQL_NSTYPE_LABEL,
- PLPGSQL_NSTYPE_VAR,
- PLPGSQL_NSTYPE_ROW,
- PLPGSQL_NSTYPE_REC
+ PLPGSQL_NSTYPE_LABEL, /* block label */
+ PLPGSQL_NSTYPE_VAR, /* scalar variable */
+ PLPGSQL_NSTYPE_REC /* composite variable */
} PLpgSQL_nsitem_type;
/*
typedef enum PLpgSQL_type_type
{
PLPGSQL_TTYPE_SCALAR, /* scalar types and domains */
- PLPGSQL_TTYPE_ROW, /* composite types */
- PLPGSQL_TTYPE_REC, /* RECORD pseudotype */
- PLPGSQL_TTYPE_PSEUDO /* other pseudotypes */
+ PLPGSQL_TTYPE_REC, /* composite types, including RECORD */
+ PLPGSQL_TTYPE_PSEUDO /* pseudotypes */
} PLpgSQL_type_type;
/*
int16 typlen; /* stuff copied from its pg_type entry */
bool typbyval;
char typtype;
- Oid typrelid;
Oid collation; /* from pg_type, but can be overridden */
bool typisarray; /* is "true" array, or domain over one */
int32 atttypmod; /* typmod (taken from someplace else) */
} PLpgSQL_var;
/*
- * Row variable
+ * Row variable - this represents one or more variables that are listed in an
+ * INTO clause, FOR-loop targetlist, cursor argument list, etc. We also use
+ * a row to represent a function's OUT parameters when there's more than one.
+ *
+ * Note that there's no way to name the row as such from PL/pgSQL code,
+ * so many functions don't need to support these.
*/
typedef struct PLpgSQL_row
{
char *refname;
int lineno;
- /* Note: TupleDesc is only set up for named rowtypes, else it is NULL. */
- TupleDesc rowtupdesc;
-
/*
- * Note: if the underlying rowtype contains a dropped column, the
- * corresponding fieldnames[] entry will be NULL, and there is no
- * corresponding var (varnos[] will be -1).
+ * rowtupdesc is only set up if we might need to convert the row into a
+ * composite datum, which currently only happens for OUT parameters.
+ * Otherwise it is NULL.
*/
+ TupleDesc rowtupdesc;
+
int nfields;
char **fieldnames;
int *varnos;
} PLpgSQL_row;
/*
- * Record variable (non-fixed structure)
+ * Record variable (any composite type, including RECORD)
*/
typedef struct PLpgSQL_rec
{
int dno;
char *refname;
int lineno;
-
- HeapTuple tup;
- TupleDesc tupdesc;
- bool freetup;
- bool freetupdesc;
+ Oid rectypeid; /* declared type of variable */
+ /* RECFIELDs for this record are chained together for easy access */
+ int firstfield; /* dno of first RECFIELD, or -1 if none */
+ /* We always store record variables as "expanded" records */
+ ExpandedRecordHeader *erh;
} PLpgSQL_rec;
/*
{
PLpgSQL_datum_type dtype;
int dno;
- char *fieldname;
+ char *fieldname; /* name of field */
int recparentno; /* dno of parent record */
+ int nextfield; /* dno of next child, or -1 if none */
+ uint64 rectupledescid; /* record's tupledesc ID as of last lookup */
+ ExpandedRecordFieldInfo finfo; /* field's attnum and type info */
+ /* if rectupledescid == INVALID_TUPLEDESC_IDENTIFIER, finfo isn't valid */
} PLpgSQL_recfield;
/*
bool readonly_func;
- TupleDesc rettupdesc;
char *exitlabel; /* the "target" label of the current EXIT or
* CONTINUE stmt, if any */
ErrorData *cur_error; /* current exception handler's error */
Tuplestorestate *tuple_store; /* SRFs accumulate results here */
+ TupleDesc tuple_store_desc; /* descriptor for tuples in tuple_store */
MemoryContext tuple_store_cxt;
ResourceOwner tuple_store_owner;
ReturnSetInfo *rsi;
int found_varno;
int ndatums;
PLpgSQL_datum **datums;
+ /* context containing variable values (same as func's SPI_proc context) */
+ MemoryContext datum_context;
/*
* paramLI is what we use to pass local variable values to the executor.
PLpgSQL_type *dtype,
bool add2namespace);
extern PLpgSQL_rec *plpgsql_build_record(const char *refname, int lineno,
- bool add2namespace);
+ Oid rectypeid, bool add2namespace);
+extern PLpgSQL_recfield *plpgsql_build_recfield(PLpgSQL_rec *rec,
+ const char *fldname);
extern int plpgsql_recognize_err_condition(const char *condname,
bool allow_sqlstate);
extern PLpgSQL_condition *plpgsql_parse_err_condition(char *condname);
--- /dev/null
+--
+-- Tests for PL/pgSQL handling of composite (record) variables
+--
+
+create type two_int4s as (f1 int4, f2 int4);
+create type two_int8s as (q1 int8, q2 int8);
+
+-- base-case return of a composite type
+create function retc(int) returns two_int8s language plpgsql as
+$$ begin return row($1,1)::two_int8s; end $$;
+select retc(42);
+
+-- ok to return a matching record type
+create or replace function retc(int) returns two_int8s language plpgsql as
+$$ begin return row($1::int8, 1::int8); end $$;
+select retc(42);
+
+-- we don't currently support implicit casting
+create or replace function retc(int) returns two_int8s language plpgsql as
+$$ begin return row($1,1); end $$;
+select retc(42);
+
+-- nor extra columns
+create or replace function retc(int) returns two_int8s language plpgsql as
+$$ begin return row($1::int8, 1::int8, 42); end $$;
+select retc(42);
+
+-- same cases with an intermediate "record" variable
+create or replace function retc(int) returns two_int8s language plpgsql as
+$$ declare r record; begin r := row($1::int8, 1::int8); return r; end $$;
+select retc(42);
+
+create or replace function retc(int) returns two_int8s language plpgsql as
+$$ declare r record; begin r := row($1,1); return r; end $$;
+select retc(42);
+
+create or replace function retc(int) returns two_int8s language plpgsql as
+$$ declare r record; begin r := row($1::int8, 1::int8, 42); return r; end $$;
+select retc(42);
+
+-- but, for mostly historical reasons, we do convert when assigning
+-- to a named-composite-type variable
+create or replace function retc(int) returns two_int8s language plpgsql as
+$$ declare r two_int8s; begin r := row($1::int8, 1::int8, 42); return r; end $$;
+select retc(42);
+
+do $$ declare c two_int8s;
+begin c := row(1,2); raise notice 'c = %', c; end$$;
+
+do $$ declare c two_int8s;
+begin for c in select 1,2 loop raise notice 'c = %', c; end loop; end$$;
+
+do $$ declare c4 two_int4s; c8 two_int8s;
+begin
+ c8 := row(1,2);
+ c4 := c8;
+ c8 := c4;
+ raise notice 'c4 = %', c4;
+ raise notice 'c8 = %', c8;
+end$$;
+
+-- check passing composite result to another function
+create function getq1(two_int8s) returns int8 language plpgsql as $$
+declare r two_int8s; begin r := $1; return r.q1; end $$;
+
+select getq1(retc(344));
+select getq1(row(1,2));
+
+do $$
+declare r1 two_int8s; r2 record; x int8;
+begin
+ r1 := retc(345);
+ perform getq1(r1);
+ x := getq1(r1);
+ raise notice 'x = %', x;
+ r2 := retc(346);
+ perform getq1(r2);
+ x := getq1(r2);
+ raise notice 'x = %', x;
+end$$;
+
+-- check assignments of composites
+do $$
+declare r1 two_int8s; r2 two_int8s; r3 record; r4 record;
+begin
+ r1 := row(1,2);
+ raise notice 'r1 = %', r1;
+ r1 := r1; -- shouldn't do anything
+ raise notice 'r1 = %', r1;
+ r2 := r1;
+ raise notice 'r1 = %', r1;
+ raise notice 'r2 = %', r2;
+ r2.q2 = r1.q1 + 3; -- check that r2 has distinct storage
+ raise notice 'r1 = %', r1;
+ raise notice 'r2 = %', r2;
+ r1 := null;
+ raise notice 'r1 = %', r1;
+ raise notice 'r2 = %', r2;
+ r1 := row(7,11)::two_int8s;
+ r2 := r1;
+ raise notice 'r1 = %', r1;
+ raise notice 'r2 = %', r2;
+ r3 := row(1,2);
+ r4 := r3;
+ raise notice 'r3 = %', r3;
+ raise notice 'r4 = %', r4;
+ r4.f1 := r4.f1 + 3; -- check that r4 has distinct storage
+ raise notice 'r3 = %', r3;
+ raise notice 'r4 = %', r4;
+ r1 := r3;
+ raise notice 'r1 = %', r1;
+ r4 := r1;
+ raise notice 'r4 = %', r4;
+ r4.q2 := r4.q2 + 1; -- r4's field names have changed
+ raise notice 'r4 = %', r4;
+end$$;
+
+-- fields of named-type vars read as null if uninitialized
+do $$
+declare r1 two_int8s;
+begin
+ raise notice 'r1 = %', r1;
+ raise notice 'r1.q1 = %', r1.q1;
+ raise notice 'r1.q2 = %', r1.q2;
+ raise notice 'r1 = %', r1;
+end$$;
+
+do $$
+declare r1 two_int8s;
+begin
+ raise notice 'r1.q1 = %', r1.q1;
+ raise notice 'r1.q2 = %', r1.q2;
+ raise notice 'r1 = %', r1;
+ raise notice 'r1.nosuchfield = %', r1.nosuchfield;
+end$$;
+
+-- records, not so much
+do $$
+declare r1 record;
+begin
+ raise notice 'r1 = %', r1;
+ raise notice 'r1.f1 = %', r1.f1;
+ raise notice 'r1.f2 = %', r1.f2;
+ raise notice 'r1 = %', r1;
+end$$;
+
+-- but OK if you assign first
+do $$
+declare r1 record;
+begin
+ raise notice 'r1 = %', r1;
+ r1 := row(1,2);
+ raise notice 'r1.f1 = %', r1.f1;
+ raise notice 'r1.f2 = %', r1.f2;
+ raise notice 'r1 = %', r1;
+ raise notice 'r1.nosuchfield = %', r1.nosuchfield;
+end$$;
+
+-- check repeated assignments to composite fields
+create table some_table (id int, data text);
+
+do $$
+declare r some_table;
+begin
+ r := (23, 'skidoo');
+ for i in 1 .. 10 loop
+ r.id := r.id + i;
+ r.data := r.data || ' ' || i;
+ end loop;
+ raise notice 'r = %', r;
+end$$;
+
+-- check behavior of function declared to return "record"
+
+create function returnsrecord(int) returns record language plpgsql as
+$$ begin return row($1,$1+1); end $$;
+
+select returnsrecord(42);
+select * from returnsrecord(42) as r(x int, y int);
+select * from returnsrecord(42) as r(x int, y int, z int); -- fail
+select * from returnsrecord(42) as r(x int, y bigint); -- fail
+
+-- same with an intermediate record variable
+create or replace function returnsrecord(int) returns record language plpgsql as
+$$ declare r record; begin r := row($1,$1+1); return r; end $$;
+
+select returnsrecord(42);
+select * from returnsrecord(42) as r(x int, y int);
+select * from returnsrecord(42) as r(x int, y int, z int); -- fail
+select * from returnsrecord(42) as r(x int, y bigint); -- fail
+
+-- should work the same with a missing column in the actual result value
+create table has_hole(f1 int, f2 int, f3 int);
+alter table has_hole drop column f2;
+
+create or replace function returnsrecord(int) returns record language plpgsql as
+$$ begin return row($1,$1+1)::has_hole; end $$;
+
+select returnsrecord(42);
+select * from returnsrecord(42) as r(x int, y int);
+select * from returnsrecord(42) as r(x int, y int, z int); -- fail
+select * from returnsrecord(42) as r(x int, y bigint); -- fail
+
+-- same with an intermediate record variable
+create or replace function returnsrecord(int) returns record language plpgsql as
+$$ declare r record; begin r := row($1,$1+1)::has_hole; return r; end $$;
+
+select returnsrecord(42);
+select * from returnsrecord(42) as r(x int, y int);
+select * from returnsrecord(42) as r(x int, y int, z int); -- fail
+select * from returnsrecord(42) as r(x int, y bigint); -- fail
+
+-- check access to a field of an argument declared "record"
+create function getf1(x record) returns int language plpgsql as
+$$ begin return x.f1; end $$;
+select getf1(1);
+select getf1(row(1,2));
+select getf1(row(1,2)::two_int8s);
+select getf1(row(1,2));
+
+-- check behavior when assignment to FOR-loop variable requires coercion
+do $$
+declare r two_int8s;
+begin
+ for r in select i, i+1 from generate_series(1,4) i
+ loop
+ raise notice 'r = %', r;
+ end loop;
+end$$;
+
+-- check behavior when returning setof composite
+create function returnssetofholes() returns setof has_hole language plpgsql as
+$$
+declare r record;
+ h has_hole;
+begin
+ return next h;
+ r := (1,2);
+ h := (3,4);
+ return next r;
+ return next h;
+ return next row(5,6);
+ return next row(7,8)::has_hole;
+end$$;
+select returnssetofholes();
+
+create or replace function returnssetofholes() returns setof has_hole language plpgsql as
+$$
+declare r record;
+begin
+ return next r; -- fails, not assigned yet
+end$$;
+select returnssetofholes();
+
+create or replace function returnssetofholes() returns setof has_hole language plpgsql as
+$$
+begin
+ return next row(1,2,3); -- fails
+end$$;
+select returnssetofholes();
+
+-- check behavior with changes of a named rowtype
+create table mutable(f1 int, f2 text);
+
+create function sillyaddone(int) returns int language plpgsql as
+$$ declare r mutable; begin r.f1 := $1; return r.f1 + 1; end $$;
+select sillyaddone(42);
+
+alter table mutable drop column f1;
+alter table mutable add column f1 float8;
+
+-- currently, this fails due to cached plan for "r.f1 + 1" expression
+select sillyaddone(42);
+\c -
+-- but it's OK after a reconnect
+select sillyaddone(42);
+
+alter table mutable drop column f1;
+select sillyaddone(42); -- fail
+
+create function getf3(x mutable) returns int language plpgsql as
+$$ begin return x.f3; end $$;
+select getf3(null::mutable); -- doesn't work yet
+alter table mutable add column f3 int;
+select getf3(null::mutable); -- now it works
+alter table mutable drop column f3;
+select getf3(null::mutable); -- fails again
+
+-- check access to system columns in a record variable
+
+create function sillytrig() returns trigger language plpgsql as
+$$begin
+ raise notice 'old.ctid = %', old.ctid;
+ raise notice 'old.tableoid = %', old.tableoid::regclass;
+ return new;
+end$$;
+
+create trigger mutable_trig before update on mutable for each row
+execute procedure sillytrig();
+
+insert into mutable values ('foo'), ('bar');
+update mutable set f2 = f2 || ' baz';
+table mutable;
+
+-- check returning a composite datum from a trigger
+
+create or replace function sillytrig() returns trigger language plpgsql as
+$$begin
+ return row(new.*);
+end$$;
+
+update mutable set f2 = f2 || ' baz';
+table mutable;
+
+create or replace function sillytrig() returns trigger language plpgsql as
+$$declare r record;
+begin
+ r := row(new.*);
+ return r;
+end$$;
+
+update mutable set f2 = f2 || ' baz';
+table mutable;
+
+--
+-- Domains of composite
+--
+
+create domain ordered_int8s as two_int8s check((value).q1 <= (value).q2);
+
+create function read_ordered_int8s(p ordered_int8s) returns int8 as $$
+begin return p.q1 + p.q2; end
+$$ language plpgsql;
+
+select read_ordered_int8s(row(1, 2));
+select read_ordered_int8s(row(2, 1)); -- fail
+
+create function build_ordered_int8s(i int8, j int8) returns ordered_int8s as $$
+begin return row(i,j); end
+$$ language plpgsql;
+
+select build_ordered_int8s(1,2);
+select build_ordered_int8s(2,1); -- fail
+
+create function build_ordered_int8s_2(i int8, j int8) returns ordered_int8s as $$
+declare r record; begin r := row(i,j); return r; end
+$$ language plpgsql;
+
+select build_ordered_int8s_2(1,2);
+select build_ordered_int8s_2(2,1); -- fail
+
+create function build_ordered_int8s_3(i int8, j int8) returns ordered_int8s as $$
+declare r two_int8s; begin r := row(i,j); return r; end
+$$ language plpgsql;
+
+select build_ordered_int8s_3(1,2);
+select build_ordered_int8s_3(2,1); -- fail
+
+create function build_ordered_int8s_4(i int8, j int8) returns ordered_int8s as $$
+declare r ordered_int8s; begin r := row(i,j); return r; end
+$$ language plpgsql;
+
+select build_ordered_int8s_4(1,2);
+select build_ordered_int8s_4(2,1); -- fail
+
+create function build_ordered_int8s_a(i int8, j int8) returns ordered_int8s[] as $$
+begin return array[row(i,j), row(i,j+1)]; end
+$$ language plpgsql;
+
+select build_ordered_int8s_a(1,2);
+select build_ordered_int8s_a(2,1); -- fail
+
+-- check field assignment
+do $$
+declare r ordered_int8s;
+begin
+ r.q1 := null;
+ r.q2 := 43;
+ r.q1 := 42;
+ r.q2 := 41; -- fail
+end$$;
+
+-- check whole-row assignment
+do $$
+declare r ordered_int8s;
+begin
+ r := null;
+ r := row(null,null);
+ r := row(1,2);
+ r := row(2,1); -- fail
+end$$;
+
+-- check assignment in for-loop
+do $$
+declare r ordered_int8s;
+begin
+ for r in values (1,2),(3,4),(6,5) loop
+ raise notice 'r = %', r;
+ end loop;
+end$$;
+
+-- check behavior with toastable fields, too
+
+create type two_texts as (f1 text, f2 text);
+create domain ordered_texts as two_texts check((value).f1 <= (value).f2);
+
+create table sometable (id int, a text, b text);
+-- b should be compressed, but in-line
+insert into sometable values (1, 'a', repeat('ffoob',1000));
+-- this b should be out-of-line
+insert into sometable values (2, 'a', repeat('ffoob',100000));
+-- this pair should fail the domain check
+insert into sometable values (3, 'z', repeat('ffoob',100000));
+
+do $$
+declare d ordered_texts;
+begin
+ for d in select a, b from sometable loop
+ raise notice 'succeeded at "%"', d.f1;
+ end loop;
+end$$;
+
+do $$
+declare r record; d ordered_texts;
+begin
+ for r in select * from sometable loop
+ raise notice 'processing row %', r.id;
+ d := row(r.a, r.b);
+ end loop;
+end$$;
+
+do $$
+declare r record; d ordered_texts;
+begin
+ for r in select * from sometable loop
+ raise notice 'processing row %', r.id;
+ d := null;
+ d.f1 := r.a;
+ d.f2 := r.b;
+ end loop;
+end$$;
/* We'll set up the per-field data later */
arg->u.tuple.recdesc = NULL;
arg->u.tuple.typentry = typentry;
- arg->u.tuple.tupdescseq = typentry ? typentry->tupDescSeqNo - 1 : 0;
+ arg->u.tuple.tupdescid = INVALID_TUPLEDESC_IDENTIFIER;
arg->u.tuple.atts = NULL;
arg->u.tuple.natts = 0;
/* Mark this invalid till needed, too */
/* We'll set up the per-field data later */
arg->u.tuple.recdesc = NULL;
arg->u.tuple.typentry = typentry;
- arg->u.tuple.tupdescseq = typentry ? typentry->tupDescSeqNo - 1 : 0;
+ arg->u.tuple.tupdescid = INVALID_TUPLEDESC_IDENTIFIER;
arg->u.tuple.atts = NULL;
arg->u.tuple.natts = 0;
}
/* We should have the descriptor of the type's typcache entry */
Assert(desc == arg->u.tuple.typentry->tupDesc);
/* Detect change of descriptor, update cache if needed */
- if (arg->u.tuple.tupdescseq != arg->u.tuple.typentry->tupDescSeqNo)
+ if (arg->u.tuple.tupdescid != arg->u.tuple.typentry->tupDesc_identifier)
{
PLy_output_setup_tuple(arg, desc,
PLy_current_execution_context()->curr_proc);
- arg->u.tuple.tupdescseq = arg->u.tuple.typentry->tupDescSeqNo;
+ arg->u.tuple.tupdescid = arg->u.tuple.typentry->tupDesc_identifier;
}
}
else
TupleDesc recdesc;
/* If we're dealing with a named composite type, these fields are set: */
TypeCacheEntry *typentry; /* typcache entry for type */
- int64 tupdescseq; /* last tupdesc seqno seen in typcache */
+ uint64 tupdescid; /* last tupdesc identifier seen in typcache */
/* These fields are NULL/0 if not yet set: */
PLyDatumToOb *atts; /* array of per-column conversion info */
int natts; /* length of array */
TupleDesc recdesc;
/* If we're dealing with a named composite type, these fields are set: */
TypeCacheEntry *typentry; /* typcache entry for type */
- int64 tupdescseq; /* last tupdesc seqno seen in typcache */
+ uint64 tupdescid; /* last tupdesc identifier seen in typcache */
/* These fields are NULL/0 if not yet set: */
PLyObToDatum *atts; /* array of per-column conversion info */
int natts; /* length of array */
x int;
y int := i;
r record;
+ c int8_tbl;
begin
if i = 1 then
x := 42;
r := row(i, i+1);
+ c := row(i, i+1);
end if;
raise notice 'x = %', x;
raise notice 'y = %', y;
raise notice 'r = %', r;
+ raise notice 'c = %', c;
end;
end loop;
end$$;
NOTICE: x = 42
NOTICE: y = 1
NOTICE: r = (1,2)
+NOTICE: c = (1,2)
NOTICE: x = <NULL>
NOTICE: y = 2
-ERROR: record "r" is not assigned yet
+NOTICE: r = <NULL>
+NOTICE: c = <NULL>
+NOTICE: x = <NULL>
+NOTICE: y = 3
+NOTICE: r = <NULL>
+NOTICE: c = <NULL>
\set VERBOSITY default
-- Check handling of conflicts between plpgsql vars and table columns.
set plpgsql.variable_conflict = error;
x int;
y int := i;
r record;
+ c int8_tbl;
begin
if i = 1 then
x := 42;
r := row(i, i+1);
+ c := row(i, i+1);
end if;
raise notice 'x = %', x;
raise notice 'y = %', y;
raise notice 'r = %', r;
+ raise notice 'c = %', c;
end;
end loop;
end$$;