Install checks in executor startup to ensure that the tuples produced by an

INSERT or UPDATE will match the target table's current rowtype.  In pre-8.3
releases inconsistency can arise with stale cached plans, as reported by
Merlin Moncure.  (We patched the equivalent hazard on the SELECT side in Feb
2007; I'm not sure why we thought there was no risk on the insertion side.)
In 8.3 and HEAD this problem should be impossible due to plan cache
invalidation management, but it seems prudent to make the check anyway.

Back-patch as far as 8.0.  7.x versions lack ALTER COLUMN TYPE, so there
seems no way to abuse a stale plan comparably.
This commit is contained in:
Tom Lane 2008-08-08 17:01:11 +00:00
parent af95d7aa63
commit 30fd8ec799
1 changed files with 88 additions and 1 deletions

View File

@ -26,7 +26,7 @@
*
*
* IDENTIFICATION
* $PostgreSQL: pgsql/src/backend/executor/execMain.c,v 1.311 2008/07/26 19:15:35 tgl Exp $
* $PostgreSQL: pgsql/src/backend/executor/execMain.c,v 1.312 2008/08/08 17:01:11 tgl Exp $
*
*-------------------------------------------------------------------------
*/
@ -47,11 +47,13 @@
#include "miscadmin.h"
#include "optimizer/clauses.h"
#include "parser/parse_clause.h"
#include "parser/parse_expr.h"
#include "parser/parsetree.h"
#include "storage/bufmgr.h"
#include "storage/lmgr.h"
#include "storage/smgr.h"
#include "utils/acl.h"
#include "utils/builtins.h"
#include "utils/lsyscache.h"
#include "utils/memutils.h"
#include "utils/snapmgr.h"
@ -72,6 +74,7 @@ typedef struct evalPlanQual
/* decls for local routines only used within this module */
static void InitPlan(QueryDesc *queryDesc, int eflags);
static void ExecCheckPlanOutput(Relation resultRel, List *targetList);
static void ExecEndPlan(PlanState *planstate, EState *estate);
static TupleTableSlot *ExecutePlan(EState *estate, PlanState *planstate,
CmdType operation,
@ -697,6 +700,9 @@ InitPlan(QueryDesc *queryDesc, int eflags)
* filter if there are any junk attrs in the tlist. UPDATE and
* DELETE always need a filter, since there's always a junk 'ctid'
* attribute present --- no need to look first.
*
* This section of code is also a convenient place to verify that the
* output of an INSERT or UPDATE matches the target table(s).
*/
{
bool junk_filter_needed = false;
@ -751,6 +757,10 @@ InitPlan(QueryDesc *queryDesc, int eflags)
PlanState *subplan = appendplans[i];
JunkFilter *j;
if (operation == CMD_UPDATE)
ExecCheckPlanOutput(resultRelInfo->ri_RelationDesc,
subplan->plan->targetlist);
j = ExecInitJunkFilter(subplan->plan->targetlist,
resultRelInfo->ri_RelationDesc->rd_att->tdhasoid,
ExecAllocTableSlot(estate->es_tupleTable));
@ -791,6 +801,10 @@ InitPlan(QueryDesc *queryDesc, int eflags)
/* Normal case with just one JunkFilter */
JunkFilter *j;
if (operation == CMD_INSERT || operation == CMD_UPDATE)
ExecCheckPlanOutput(estate->es_result_relation_info->ri_RelationDesc,
planstate->plan->targetlist);
j = ExecInitJunkFilter(planstate->plan->targetlist,
tupType->tdhasoid,
ExecAllocTableSlot(estate->es_tupleTable));
@ -827,6 +841,10 @@ InitPlan(QueryDesc *queryDesc, int eflags)
}
else
{
if (operation == CMD_INSERT)
ExecCheckPlanOutput(estate->es_result_relation_info->ri_RelationDesc,
planstate->plan->targetlist);
estate->es_junkFilter = NULL;
if (estate->es_rowMarks)
elog(ERROR, "SELECT FOR UPDATE/SHARE, but no junk columns");
@ -974,6 +992,75 @@ InitResultRelInfo(ResultRelInfo *resultRelInfo,
ExecOpenIndices(resultRelInfo);
}
/*
* Verify that the tuples to be produced by INSERT or UPDATE match the
* target relation's rowtype
*
* We do this to guard against stale plans. If plan invalidation is
* functioning properly then we should never get a failure here, but better
* safe than sorry. Note that this is called after we have obtained lock
* on the target rel, so the rowtype can't change underneath us.
*
* The plan output is represented by its targetlist, because that makes
* handling the dropped-column case easier.
*/
static void
ExecCheckPlanOutput(Relation resultRel, List *targetList)
{
TupleDesc resultDesc = RelationGetDescr(resultRel);
int attno = 0;
ListCell *lc;
foreach(lc, targetList)
{
TargetEntry *tle = (TargetEntry *) lfirst(lc);
Form_pg_attribute attr;
if (tle->resjunk)
continue; /* ignore junk tlist items */
if (attno >= resultDesc->natts)
ereport(ERROR,
(errcode(ERRCODE_DATATYPE_MISMATCH),
errmsg("table row type and query-specified row type do not match"),
errdetail("Query has too many columns.")));
attr = resultDesc->attrs[attno++];
if (!attr->attisdropped)
{
/* Normal case: demand type match */
if (exprType((Node *) tle->expr) != attr->atttypid)
ereport(ERROR,
(errcode(ERRCODE_DATATYPE_MISMATCH),
errmsg("table row type and query-specified row type do not match"),
errdetail("Table has type %s at ordinal position %d, but query expects %s.",
format_type_be(attr->atttypid),
attno,
format_type_be(exprType((Node *) tle->expr)))));
}
else
{
/*
* For a dropped column, we can't check atttypid (it's likely 0).
* In any case the planner has most likely inserted an INT4 null.
* What we insist on is just *some* NULL constant.
*/
if (!IsA(tle->expr, Const) ||
!((Const *) tle->expr)->constisnull)
ereport(ERROR,
(errcode(ERRCODE_DATATYPE_MISMATCH),
errmsg("table row type and query-specified row type do not match"),
errdetail("Query provides a value for a dropped column at ordinal position %d.",
attno)));
}
}
if (attno != resultDesc->natts)
ereport(ERROR,
(errcode(ERRCODE_DATATYPE_MISMATCH),
errmsg("table row type and query-specified row type do not match"),
errdetail("Query has too few columns.")));
}
/*
* ExecGetTriggerResultRel
*