Re: TupleTableSlot abstraction

From: Andres Freund <andres(at)anarazel(dot)de>
To: Ashutosh Bapat <ashutosh(dot)bapat(at)enterprisedb(dot)com>
Cc: pgsql-hackers <pgsql-hackers(at)postgresql(dot)org>, Robert Haas <robertmhaas(at)gmail(dot)com>, Haribabu Kommi <kommi(dot)haribabu(at)gmail(dot)com>, Alvaro Herrera <alvherre(at)2ndquadrant(dot)com>, Amit Kapila <amit(dot)kapila16(at)gmail(dot)com>
Subject: Re: TupleTableSlot abstraction
Date: 2018-08-24 01:16:34
Message-ID: 20180824011634.q2lrc6b26mjwoi5s@alap3.anarazel.de
Views: Raw Message | Whole Thread | Download mbox | Resend email
Thread:
Lists: pgsql-hackers

Hi,

On 2018-08-20 19:51:33 +0530, Ashutosh Bapat wrote:
> Sorry, forgot about that. Here's the patch set with that addressed.

Btw, you attach files as tar.zip, but they're actually gzip
compressed...

> From 838a463646a048b3dccff95079a514fdc86effb3 Mon Sep 17 00:00:00 2001
> From: Ashutosh Bapat <ashutosh(dot)bapat(at)enterprisedb(dot)com>
> Date: Mon, 13 Aug 2018 11:27:57 +0530
> Subject: [PATCH 01/11] Split ExecStoreTuple into ExecStoreHeapTuple and
> ExecStoreBufferHeapTuple
>
> ExecStoreTuple() accepts a heap tuple from a buffer or constructed
> on-the-fly. In the first case the caller passed a valid buffer and in
> the later case it passes InvalidBuffer. In the first case,
> ExecStoreTuple() pins the given buffer and in the later case it
> records shouldFree flag. The function has some extra checks to
> differentiate between the two cases. The usecases never overlap thus
> spending extra cycles in checks is useless. Hence separate these
> usecases into separate functions ExecStoreHeapTuple() to store
> on-the-fly tuple and ExecStoreBufferHeapTuple() to store an on-disk
> tuple from a buffer. This allows to shave some extra cycles while
> storing a tuple in the slot.

It doesn't *yet* allow shaving extra cycles, no?

> * SLOT ACCESSORS
> * ExecSetSlotDescriptor - set a slot's tuple descriptor
> - * ExecStoreTuple - store a physical tuple in the slot
> + * ExecStoreHeapTuple - store an on-the-fly heap tuple in the slot
> + * ExecStoreBufferHeapTuple - store an on-disk heap tuple in the slot
> * ExecStoreMinimalTuple - store a minimal physical tuple in the slot
> * ExecClearTuple - clear contents of a slot
> * ExecStoreVirtualTuple - mark slot as containing a virtual
> * tuple

I'd advocate for a separate patch ripping these out, they're almost
always out of date.

> /* --------------------------------
> - * ExecStoreTuple
> + * ExecStoreHeapTuple
> *
> - * This function is used to store a physical tuple into a specified
> + * This function is used to store an on-the-fly physical tuple into a specified
> * slot in the tuple table.
> *
> * tuple: tuple to store
> * slot: slot to store it in
> - * buffer: disk buffer if tuple is in a disk page, else InvalidBuffer
> * shouldFree: true if ExecClearTuple should pfree() the tuple
> * when done with it
> *
> - * If 'buffer' is not InvalidBuffer, the tuple table code acquires a pin
> - * on the buffer which is held until the slot is cleared, so that the tuple
> - * won't go away on us.
> + * shouldFree is normally set 'true' for tuples constructed on-the-fly. But it
> + * can be 'false' when the referenced tuple is held in a tuple table slot
> + * belonging to a lower-level executor Proc node. In this case the lower-level
> + * slot retains ownership and responsibility for eventually releasing the
> + * tuple. When this method is used, we must be certain that the upper-level
> + * Proc node will lose interest in the tuple sooner than the lower-level one
> + * does! If you're not certain, copy the lower-level tuple with heap_copytuple
> + * and let the upper-level table slot assume ownership of the copy!
> + *
> + * Return value is just the passed-in slot pointer.
> + * --------------------------------
> + */
> +TupleTableSlot *
> +ExecStoreHeapTuple(HeapTuple tuple,
> + TupleTableSlot *slot,
> + bool shouldFree)
> +{
> + /*
> + * sanity checks
> + */
> + Assert(tuple != NULL);
> + Assert(slot != NULL);
> + Assert(slot->tts_tupleDescriptor != NULL);
> +
> + /*
> + * Free any old physical tuple belonging to the slot.
> + */
> + if (slot->tts_shouldFree)
> + heap_freetuple(slot->tts_tuple);
> + if (slot->tts_shouldFreeMin)
> + heap_free_minimal_tuple(slot->tts_mintuple);
> +
> + /*
> + * Store the new tuple into the specified slot.
> + */
> + slot->tts_isempty = false;
> + slot->tts_shouldFree = shouldFree;
> + slot->tts_shouldFreeMin = false;
> + slot->tts_tuple = tuple;
> + slot->tts_mintuple = NULL;
> +
> + /* Mark extracted state invalid */
> + slot->tts_nvalid = 0;
> +
> + return slot;
> +}

Uh, there could very well be a buffer previously stored in the slot, no?
This can't currently be applied independently afaict.

> From c4c55bc0d501400ffca2e7393039b2d38660cd2d Mon Sep 17 00:00:00 2001
> From: Ashutosh Bapat <ashutosh(dot)bapat(at)enterprisedb(dot)com>
> Date: Mon, 13 Aug 2018 11:27:57 +0530
> Subject: [PATCH 02/11] tts_nvalid in TupleTableSlot is AttrNumber

> @@ -125,7 +125,7 @@ typedef struct TupleTableSlot
> MemoryContext tts_mcxt; /* slot itself is in this context */
> Buffer tts_buffer; /* tuple's buffer, or InvalidBuffer */
> #define FIELDNO_TUPLETABLESLOT_NVALID 9
> - int tts_nvalid; /* # of valid values in tts_values */
> + AttrNumber tts_nvalid; /* # of valid values in tts_values */
> #define FIELDNO_TUPLETABLESLOT_VALUES 10
> Datum *tts_values; /* current per-attribute values */
> #define FIELDNO_TUPLETABLESLOT_ISNULL 11

Shouldn't this be adapting at least a *few* more things, like
slot_getattr's argument?

> /*
> - * Fill in missing values for a TupleTableSlot.
> - *
> - * This is only exposed because it's needed for JIT compiled tuple
> - * deforming. That exception aside, there should be no callers outside of this
> - * file.
> - */
> -void
> -slot_getmissingattrs(TupleTableSlot *slot, int startAttNum, int lastAttNum)
> -{
> - AttrMissing *attrmiss = NULL;
> - int missattnum;
> -
> - if (slot->tts_tupleDescriptor->constr)
> - attrmiss = slot->tts_tupleDescriptor->constr->missing;
> -
> - if (!attrmiss)
> - {
> - /* no missing values array at all, so just fill everything in as NULL */
> - memset(slot->tts_values + startAttNum, 0,
> - (lastAttNum - startAttNum) * sizeof(Datum));
> - memset(slot->tts_isnull + startAttNum, 1,
> - (lastAttNum - startAttNum) * sizeof(bool));
> - }
> - else
> - {
> - /* if there is a missing values array we must process them one by one */
> - for (missattnum = startAttNum;
> - missattnum < lastAttNum;
> - missattnum++)
> - {
> - slot->tts_values[missattnum] = attrmiss[missattnum].am_value;
> - slot->tts_isnull[missattnum] = !attrmiss[missattnum].am_present;
> - }
> - }
> -}

I would split out these moves into a separate commit, they are trivially
committable separately. The commit's pretty big already, and that'd make
it easier to see the actual differences.

> -
> -/*
> * heap_compute_data_size
> * Determine size of the data area of a tuple to be constructed
> */
> @@ -1407,10 +1370,9 @@ heap_deform_tuple(HeapTuple tuple, TupleDesc tupleDesc,
> * re-computing information about previously extracted attributes.
> * slot->tts_nvalid is the number of attributes already extracted.
> */
> -static void
> -slot_deform_tuple(TupleTableSlot *slot, int natts)
> +void
> +slot_deform_tuple(TupleTableSlot *slot, HeapTuple tuple, uint32 *offp, int natts)
> {

This should be renamed to include "heap" in the name, as it's not going
to be usable for, say, zheap.

> -/*
> - * slot_getsysattr
> - * This function fetches a system attribute of the slot's current tuple.
> - * Unlike slot_getattr, if the slot does not contain system attributes,
> - * this will return false (with a NULL attribute value) instead of
> - * throwing an error.
> - */
> -bool
> -slot_getsysattr(TupleTableSlot *slot, int attnum,
> - Datum *value, bool *isnull)
> -{
> - HeapTuple tuple = slot->tts_tuple;
> -
> - Assert(attnum < 0); /* else caller error */
> - if (tuple == NULL ||
> - tuple == &(slot->tts_minhdr))
> - {
> - /* No physical tuple, or minimal tuple, so fail */
> - *value = (Datum) 0;
> - *isnull = true;
> - return false;
> - }
> - *value = heap_getsysattr(tuple, attnum, slot->tts_tupleDescriptor, isnull);
> - return true;
> -}

I think I was wrong at saying that we should remove this. I think you
were right that it should become a callback...

> +/*
> + * This is a function used by all getattr() callbacks which deal with a heap
> + * tuple or some tuple format which can be represented as a heap tuple e.g. a
> + * minimal tuple.
> + *
> + * heap_getattr considers any attnum beyond the attributes available in the
> + * tuple as NULL. This function however returns the values of missing
> + * attributes from the tuple descriptor in that case. Also this function does
> + * not support extracting system attributes.
> + *
> + * If the attribute needs to be fetched from the tuple, the function fills in
> + * tts_values and tts_isnull arrays upto the required attnum.
> + */
> +Datum
> +tts_heap_getattr_common(TupleTableSlot *slot, HeapTuple tuple, uint32 *offp,
> + int attnum, bool *isnull)
> +{
> + HeapTupleHeader tup = tuple->t_data;
> + Assert(slot->tts_nvalid < attnum);
> +
> + Assert(attnum > 0);
> +
> + if (attnum > HeapTupleHeaderGetNatts(tup))
> + return getmissingattr(slot->tts_tupleDescriptor, attnum, isnull);
> +
> + /*
> + * check if target attribute is null: no point in groveling through tuple
> + */
> + if (HeapTupleHasNulls(tuple) && att_isnull(attnum - 1, tup->t_bits))
> + {
> + *isnull = true;
> + return (Datum) 0;
> + }

I still think this is an optimization with a negative benefit,
especially as it requires an extra callback. We should just rely on
slot_deform_tuple and then access that. That'll also just access the
null bitmap for the relevant column, and it'll make successive accesses
cheaper.

> @@ -2883,7 +2885,7 @@ CopyFrom(CopyState cstate)
> if (slot == NULL) /* "do nothing" */
> skip_tuple = true;
> else /* trigger might have changed tuple */
> - tuple = ExecMaterializeSlot(slot);
> + tuple = ExecFetchSlotTuple(slot, true);
> }

Could we do the Materialize vs Fetch vs Copy change separately?

> diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c
> index e1eb7c3..9957c70 100644
> --- a/src/backend/commands/matview.c
> +++ b/src/backend/commands/matview.c
> @@ -484,7 +484,7 @@ transientrel_receive(TupleTableSlot *slot, DestReceiver *self)
> * get the heap tuple out of the tuple table slot, making sure we have a
> * writable copy
> */
> - tuple = ExecMaterializeSlot(slot);
> + tuple = ExecCopySlotTuple(slot);
>
> heap_insert(myState->transientrel,
> tuple,
> @@ -494,6 +494,9 @@ transientrel_receive(TupleTableSlot *slot, DestReceiver *self)
>
> /* We know this is a newly created relation, so there are no indexes */
>
> + /* Free the copied tuple. */
> + heap_freetuple(tuple);
> +
> return true;
> }

This'll potentially increase a fair amount of extra allocation overhead,
no?

> diff --git a/src/backend/executor/execExprInterp.c b/src/backend/executor/execExprInterp.c
> index 9d6e25a..1b4e726 100644
> --- a/src/backend/executor/execExprInterp.c
> +++ b/src/backend/executor/execExprInterp.c
> @@ -490,54 +490,21 @@ ExecInterpExpr(ExprState *state, ExprContext *econtext, bool *isnull)
>
> EEO_CASE(EEOP_INNER_SYSVAR)
> {
> - int attnum = op->d.var.attnum;
> - Datum d;
> -
> - /* these asserts must match defenses in slot_getattr */
> - Assert(innerslot->tts_tuple != NULL);
> - Assert(innerslot->tts_tuple != &(innerslot->tts_minhdr));
> -
> - /* heap_getsysattr has sufficient defenses against bad attnums */
> - d = heap_getsysattr(innerslot->tts_tuple, attnum,
> - innerslot->tts_tupleDescriptor,
> - op->resnull);
> - *op->resvalue = d;
> + ExecEvalSysVar(state, op, econtext, innerslot);

Please split this out into a separate patch.

> +/*
> + * TupleTableSlotOps implementation for BufferHeapTupleTableSlot.
> + */
> +
> +static void
> +tts_buffer_init(TupleTableSlot *slot)
> +{
> +}

Should rename these to buffer_heap or such.

> +/*
> + * Store the given tuple into the given BufferHeapTupleTableSlot and pin the
> + * given buffer. If the tuple already contained in the slot can be freed free
> + * it.
> + */
> +static void
> +tts_buffer_store_tuple(TupleTableSlot *slot, HeapTuple tuple, Buffer buffer)
> +{
> + BufferHeapTupleTableSlot *bslot = (BufferHeapTupleTableSlot *) slot;
> +
> + if (IS_TTS_SHOULDFREE(slot))
> + {
> + /*
> + * A heap tuple stored in a BufferHeapTupleTableSlot should have a
> + * buffer associated with it, unless it's materialized.
> + */
> + Assert(!BufferIsValid(bslot->buffer));
> +
> + heap_freetuple(bslot->base.tuple);
> + RESET_TTS_SHOULDFREE(slot);
> + }
> +
> + RESET_TTS_EMPTY(slot);
> + slot->tts_nvalid = 0;
> + bslot->base.tuple = tuple;
> + bslot->base.off = 0;
> +
> + /*
> + * If tuple is on a disk page, keep the page pinned as long as we hold a
> + * pointer into it. We assume the caller already has such a pin.
> + *
> + * This is coded to optimize the case where the slot previously held a
> + * tuple on the same disk page: in that case releasing and re-acquiring
> + * the pin is a waste of cycles. This is a common situation during
> + * seqscans, so it's worth troubling over.
> + */
> + if (bslot->buffer != buffer)
> + {
> + if (BufferIsValid(bslot->buffer))
> + ReleaseBuffer(bslot->buffer);
> + bslot->buffer = buffer;
> + IncrBufferRefCount(buffer);
> + }
> +}

This needs to also support storing a non-buffer tuple, I think.

> +/*
> + * TupleTableSlotOps for each of TupleTableSlotTypes. These are used to
> + * identify the type of slot.
> + */
> +const TupleTableSlotOps TTSOpsVirtual = {
> + sizeof(TupleTableSlot),
> + tts_virtual_init,
> + tts_virtual_release,
> + tts_virtual_clear,
> + tts_virtual_getsomeattrs,
> + tts_virtual_attisnull,
> + tts_virtual_getattr,
> + tts_virtual_materialize,
> + tts_virtual_copyslot,
> +
> + /*
> + * A virtual tuple table slot can not "own" a heap tuple or a minimal
> + * tuple.
> + */
> + NULL,
> + NULL,
> + tts_virtual_copy_heap_tuple,
> + tts_virtual_copy_minimal_tuple,
> +};

As we're now going to require C99, could you convert these into
designated initializer style (i.e. .init = tts_heap_init etc)?

> @@ -353,25 +1285,9 @@ ExecStoreHeapTuple(HeapTuple tuple,
> Assert(slot != NULL);
> Assert(slot->tts_tupleDescriptor != NULL);
>
> - /*
> - * Free any old physical tuple belonging to the slot.
> - */
> - if (IS_TTS_SHOULDFREE(slot))
> - heap_freetuple(slot->tts_tuple);
> - if (IS_TTS_SHOULDFREEMIN(slot))
> - heap_free_minimal_tuple(slot->tts_mintuple);
> -
> - /*
> - * Store the new tuple into the specified slot.
> - */
> - RESET_TTS_EMPTY(slot);
> - shouldFree ? SET_TTS_SHOULDFREE(slot) : RESET_TTS_SHOULDFREE(slot);
> - RESET_TTS_SHOULDFREEMIN(slot);
> - slot->tts_tuple = tuple;
> - slot->tts_mintuple = NULL;
> -
> - /* Mark extracted state invalid */
> - slot->tts_nvalid = 0;
> + if (!TTS_IS_HEAPTUPLE(slot))
> + elog(ERROR, "trying to store a heap tuple into wrong type of slot");
> + tts_heap_store_tuple(slot, tuple, shouldFree);
>
> return slot;
> }

This should allow for buffer tuples too.

> @@ -983,9 +1595,10 @@ ExecInitExtraTupleSlot(EState *estate, TupleDesc tupledesc)
> * ----------------
> */
> TupleTableSlot *
> -ExecInitNullTupleSlot(EState *estate, TupleDesc tupType)
> +ExecInitNullTupleSlot(EState *estate, TupleDesc tupType,
> + const TupleTableSlotOps *tts_cb)
> {
> - TupleTableSlot *slot = ExecInitExtraTupleSlot(estate, tupType);
> + TupleTableSlot *slot = ExecInitExtraTupleSlot(estate, tupType, tts_cb);
>
> return ExecStoreAllNullTuple(slot);
> }

It's a bit weird that the type name is *Ops but the param is tts_cb, no?

> @@ -1590,7 +1590,8 @@ ExecHashTableInsert(HashJoinTable hashtable,
> TupleTableSlot *slot,
> uint32 hashvalue)
> {
> - MinimalTuple tuple = ExecFetchSlotMinimalTuple(slot);
> + bool shouldFree;
> + MinimalTuple tuple = ExecFetchSlotMinimalTuple(slot, &shouldFree);
> int bucketno;
> int batchno;
>
> @@ -1664,6 +1665,9 @@ ExecHashTableInsert(HashJoinTable hashtable,
> hashvalue,
> &hashtable->innerBatchFile[batchno]);
> }
> +
> + if (shouldFree)
> + heap_free_minimal_tuple(tuple);
> }

Hm, how about splitting these out?

> @@ -277,10 +277,32 @@ ExecInsert(ModifyTableState *mtstate,
> OnConflictAction onconflict = node->onConflictAction;
>
> /*
> - * get the heap tuple out of the tuple table slot, making sure we have a
> - * writable copy
> + * Get the heap tuple out of the tuple table slot, making sure we have a
> + * writable copy.
> + *
> + * If the slot can contain a heap tuple, materialize the tuple within the
> + * slot itself so that the slot "owns" it and any changes to the tuple
> + * reflect in the slot as well.
> + *
> + * Otherwise, store the copy of the heap tuple in es_dml_input_tuple_slot, which
> + * is assumed to be able to hold a heap tuple, so that repeated requests
> + * for heap tuple in the following code will not create multiple copies and
> + * leak memory. Also keeping the tuple in the slot makes sure that it will
> + * be freed when it's no more needed either because a trigger modified it
> + * or when we are done processing it.
> */
> - tuple = ExecMaterializeSlot(slot);
> + if (!(TTS_IS_HEAPTUPLE(slot) || TTS_IS_BUFFERTUPLE(slot)))
> + {
> + TupleTableSlot *es_slot = estate->es_dml_input_tuple_slot;
> +
> + Assert(es_slot && TTS_IS_HEAPTUPLE(es_slot));
> + if (es_slot->tts_tupleDescriptor != slot->tts_tupleDescriptor)
> + ExecSetSlotDescriptor(es_slot, slot->tts_tupleDescriptor);
> + ExecCopySlot(es_slot, slot);
> + slot = es_slot;
> + }
> +
> + tuple = ExecFetchSlotTuple(slot, true);

I strongly dislike this. Could you look at my pluggable storage tree
and see whether you could do something similar here?

> @@ -2402,8 +2454,15 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags)
> */
> mtstate->ps.plan->targetlist = (List *) linitial(node->returningLists);
>
> - /* Set up a slot for the output of the RETURNING projection(s) */
> - ExecInitResultTupleSlotTL(estate, &mtstate->ps);
> + /*
> + * Set up a slot for the output of the RETURNING projection(s).
> + * ExecDelete() requies the contents of the slot to be
> + * saved/materialized, so use heap tuple table slot for a DELETE.
> + * Otherwise a virtual tuple table slot suffices.
> + */
> + ExecInitResultTupleSlotTL(estate, &mtstate->ps,
> + operation == CMD_DELETE ?
> + &TTSOpsHeapTuple : &TTSOpsVirtual);
> slot = mtstate->ps.ps_ResultTupleSlot;

I'm not clear on why this this the case?

> /* Need a
> diff --git a/src/backend/executor/tqueue.c b/src/backend/executor/tqueue.c
> index ecdbe7f..ea2858b 100644
> --- a/src/backend/executor/tqueue.c
> +++ b/src/backend/executor/tqueue.c
> @@ -56,11 +56,28 @@ tqueueReceiveSlot(TupleTableSlot *slot, DestReceiver *self)
> TQueueDestReceiver *tqueue = (TQueueDestReceiver *) self;
> HeapTuple tuple;
> shm_mq_result result;
> + bool tuple_copied = false;
> +
> + /* Get the tuple out of slot, if necessary converting the slot's contents
> + * into a heap tuple by copying. In the later case we need to free the copy.
> + */
> + if (TTS_IS_HEAPTUPLE(slot) || TTS_IS_BUFFERTUPLE(slot))
> + {
> + tuple = ExecFetchSlotTuple(slot, true);
> + tuple_copied = false;
> + }
> + else
> + {
> + tuple = ExecCopySlotTuple(slot);
> + tuple_copied = true;
> + }

To me needing this if() here is a bad sign, I think we want a
ExecFetchSlotTuple* like api with a bool *shouldFree arg, like you did
for minimal tuples instead.

> diff --git a/src/backend/tcop/pquery.c b/src/backend/tcop/pquery.c
> index 66cc5c3..b44438b 100644
> --- a/src/backend/tcop/pquery.c
> +++ b/src/backend/tcop/pquery.c
> @@ -1071,7 +1071,7 @@ RunFromStore(Portal portal, ScanDirection direction, uint64 count,
> uint64 current_tuple_count = 0;
> TupleTableSlot *slot;
>
> - slot = MakeSingleTupleTableSlot(portal->tupDesc);
> + slot = MakeSingleTupleTableSlot(portal->tupDesc, &TTSOpsMinimalTuple);
>
> dest->rStartup(dest, CMD_SELECT, portal->tupDesc);

These fairly rote changes make it really hard to see the actual meat of
the changes in the patch. I think one way to address that would be to
introduce stub &TTSOps* variables, add them to MakeSingleTupleTableSlot
etc, but still have them return the "old style" slots. Then that can be
reviewed separately.

> @@ -1375,9 +1376,9 @@ hypothetical_dense_rank_final(PG_FUNCTION_ARGS)
> * previous row available for comparisons. This is accomplished by
> * swapping the slot pointer variables after each row.
> */
> - extraslot = MakeSingleTupleTableSlot(osastate->qstate->tupdesc);
> + extraslot = MakeSingleTupleTableSlot(osastate->qstate->tupdesc,
> + &TTSOpsMinimalTuple);
> slot2 = extraslot;
> -
> /* iterate till we find the hypothetical row */
> while (tuplesort_gettupleslot(osastate->sortstate, true, true, slot,
> &abbrevVal))

superflous change.

> diff --git a/src/backend/utils/sort/tuplestore.c b/src/backend/utils/sort/tuplestore.c
> index 5560a3e..ba98908 100644
> --- a/src/backend/utils/sort/tuplestore.c
> +++ b/src/backend/utils/sort/tuplestore.c
> @@ -1073,6 +1073,13 @@ tuplestore_gettuple(Tuplestorestate *state, bool forward,
> * pointer to a tuple held within the tuplestore. The latter is more
> * efficient but the slot contents may be corrupted if additional writes to
> * the tuplestore occur. (If using tuplestore_trim, see comments therein.)
> + *
> + * If the given slot can not contain a minimal tuple, the given minimal tuple
> + * is converted into the form that the given slot can contain. Irrespective of
> + * the value of copy, that conversion might need to create a copy. If
> + * should_free is set to true by tuplestore_gettuple(), the minimal tuple is
> + * freed after the conversion, if necessary. Right now, the function only
> + * supports slots of type HeapTupleTableSlot, other than MinimalTupleTableSlot.
> */
> bool
> tuplestore_gettupleslot(Tuplestorestate *state, bool forward,
> @@ -1085,12 +1092,25 @@ tuplestore_gettupleslot(Tuplestorestate *state, bool forward,
>
> if (tuple)
> {
> - if (copy && !should_free)
> + if (TTS_IS_MINIMALTUPLE(slot))
> {
> - tuple = heap_copy_minimal_tuple(tuple);
> + if (copy && !should_free)
> + {
> + tuple = heap_copy_minimal_tuple(tuple);
> + should_free = true;
> + }
> + ExecStoreMinimalTuple(tuple, slot, should_free);
> + }
> + else if (TTS_IS_HEAPTUPLE(slot))
> + {
> + HeapTuple htup = heap_tuple_from_minimal_tuple(tuple);
> +
> + if (should_free)
> + heap_free_minimal_tuple(tuple);
> should_free = true;
> +
> + ExecStoreHeapTuple(htup, slot, should_free);
> }
> - ExecStoreMinimalTuple(tuple, slot, should_free);
> return true;
> }
> else

Why is this a good idea? Shouldn't the caller always have a minimal slot
here? This is problematic, because it means this'd need adapting for
every new slot type, which'd be kinda against the idea of the whole
thing...

> +#define TTS_IS_VIRTUAL(slot) ((slot)->tts_cb == &TTSOpsVirtual)
> +#define TTS_IS_HEAPTUPLE(slot) ((slot)->tts_cb == &TTSOpsHeapTuple)
> +#define TTS_IS_MINIMALTUPLE(slot) ((slot)->tts_cb == &TTSOpsMinimalTuple)
> +#define TTS_IS_BUFFERTUPLE(slot) ((slot)->tts_cb == &TTSOpsBufferTuple)
> +
> +extern Datum ExecFetchSlotTupleDatum(TupleTableSlot *slot);

this is a weird place for the function protoype?

> +/* TupleTableSlotType specific routines */
> +typedef struct TupleTableSlotOps
> +{
> + /* Minimum size of the slot */
> + size_t base_slot_size;
> +
> + /* Initialization. */
> + void (*init)(TupleTableSlot *slot);
> +
> + /* Destruction. */
> + void (*release)(TupleTableSlot *slot);
> +
> + /*
> + * Clear the contents of the slot. Only the contents are expected to be
> + * cleared and not the tuple descriptor. Typically an implementation of
> + * this callback should free the memory allocated for the tuple contained
> + * in the slot.
> + */
> + void (*clear)(TupleTableSlot *slot);
> +
> + /*
> + * Fill up first natts entries of tts_values and tts_isnull arrays with
> + * values from the tuple contained in the slot. The function may be called
> + * with natts more than the number of attributes available in the tuple, in
> + * which case it should fill up as many entries as the number of available
> + * attributes. The callback should update tts_nvalid with number of entries
> + * filled up.
> + */
> + void (*getsomeattrs)(TupleTableSlot *slot, int natts);
> +
> + /*
> + * Returns true if the attribute given by attnum is NULL, return false
> + * otherwise. Some slot types may have more efficient methods to return
> + * NULL-ness of a given attribute compared to checking NULL-ness after
> + * calling getsomeattrs(). So this is a separate callback. We expect this
> + * callback to be invoked by slot_attisnull() only. That function returns
> + * if the information is available readily e.g. in tts_isnull array. The
> + * callback need not repeat the same.
> + */
> + bool (*attisnull)(TupleTableSlot *slot, int attnum);
> +
> + /*
> + * Returns value of the given attribute as a datum and sets isnull to
> + * false, if it's not NULL. If the attribute is NULL, it sets isnull to
> + * true. Some slot types may have more efficient methods to return value of
> + * a given attribute rather than returning the attribute value from
> + * tts_values and tts_isnull after calling getsomeattrs(). So this is a
> + * separate callback. We expect this callback to be invoked by
> + * slot_getattr() only. That function returns if the information is
> + * available readily e.g. in tts_values and tts_isnull array or can be
> + * inferred from tuple descriptor. The callback need not repeat the same.
> + */
> + Datum (*getattr)(TupleTableSlot *slot, int attnum, bool *isnull);

These two really show go.

> +/* virtual or base type */
> +struct TupleTableSlot
> {
> NodeTag type;
> #define FIELDNO_TUPLETABLESLOT_FLAGS 1
> - uint16 tts_flags; /* Boolean states */
> -#define FIELDNO_TUPLETABLESLOT_TUPLE 2
> - HeapTuple tts_tuple; /* physical tuple, or NULL if virtual */
> -#define FIELDNO_TUPLETABLESLOT_TUPLEDESCRIPTOR 3
> - TupleDesc tts_tupleDescriptor; /* slot's tuple descriptor */
> - MemoryContext tts_mcxt; /* slot itself is in this context */
> - Buffer tts_buffer; /* tuple's buffer, or InvalidBuffer */
> -#define FIELDNO_TUPLETABLESLOT_NVALID 6
> + uint16 tts_flags;
> +#define FIELDNO_TUPLETABLESLOT_NVALID 2
> AttrNumber tts_nvalid; /* # of valid values in tts_values */
> -#define FIELDNO_TUPLETABLESLOT_VALUES 7
> +
> + const TupleTableSlotOps *const tts_cb;
> +#define FIELDNO_TUPLETABLESLOT_TUPLEDESCRIPTOR 4
> + TupleDesc tts_tupleDescriptor; /* slot's tuple descriptor */
> +#define FIELDNO_TUPLETABLESLOT_VALUES 5
> Datum *tts_values; /* current per-attribute values */
> -#define FIELDNO_TUPLETABLESLOT_ISNULL 8
> +#define FIELDNO_TUPLETABLESLOT_ISNULL 6
> bool *tts_isnull; /* current per-attribute isnull flags */
> - MinimalTuple tts_mintuple; /* minimal tuple, or NULL if none */
> - HeapTupleData tts_minhdr; /* workspace for minimal-tuple-only case */
> -#define FIELDNO_TUPLETABLESLOT_OFF 11
> - uint32 tts_off; /* saved state for slot_deform_tuple */
> -} TupleTableSlot;
>
> -#define TTS_HAS_PHYSICAL_TUPLE(slot) \
> - ((slot)->tts_tuple != NULL && (slot)->tts_tuple != &((slot)->tts_minhdr))
> + /* can we optimize away? */
> + MemoryContext tts_mcxt; /* slot itself is in this context */
> +};

the "can we" comment above is pretty clearly wrong, I think (Yes, I made
it...).

> From 84046126d5b8c9d780cb7134048cc717bda462a8 Mon Sep 17 00:00:00 2001
> From: Ashutosh Bapat <ashutosh(dot)bapat(at)enterprisedb(dot)com>
> Date: Mon, 13 Aug 2018 11:27:57 +0530
> Subject: [PATCH 07/11] Reset expression context just after resetting per tuple
> context in ExecModifyTable().
>
> Expression context saved in ps_ExprContext for ModifyTable node is
> used to process returning clause and on conflict clause. For every
> processed tuple it's reset in ExecProcessReturning() and
> ExecOnConflictUpdate(). When a query has both RETURNING and ON
> CONFLICT clauses, the reset happens twice and the first one of those
> might reset memory used by the other. For some reason this doesn't
> show up on HEAD, but is apparent when virtual tuple table slots, which
> do not copy the datums in its own memory, are used for tuples returned
> by RETURNING clause.
>
> This is fix for a query failing in sql/insert_conflict.sql
>
> insert into insertconflicttest as i values (23, 'Avocado') on conflict (key) do update set fruit = excluded.*::text
> returning *;
>
> Ashutosh Bapat, per suggestion by Andres Freund

Doesn't this have to be earlier in the series?

Phew, sorry if some things are daft, I'm kinda jetlagged...

- Andres

In response to

Responses

Browse pgsql-hackers by date

  From Date Subject
Next Message Peter Geoghegan 2018-08-24 01:27:54 Re: [HACKERS] Re: Improve OR conditions on joined columns (common star schema problem)
Previous Message Thomas Munro 2018-08-24 00:46:02 Re: [HACKERS] Optional message to user when terminating/cancelling backend