Ensure that whole-row Vars produce nonempty column names.
authorTom Lane
Mon, 10 Nov 2014 20:21:20 +0000 (15:21 -0500)
committerTom Lane
Mon, 10 Nov 2014 20:21:20 +0000 (15:21 -0500)
At one time it wasn't terribly important what column names were associated
with the fields of a composite Datum, but since the introduction of
operations like row_to_json(), it's important that looking up the rowtype
ID embedded in the Datum returns the column names that users would expect.
However, that doesn't work terribly well: you could get the column names
of the underlying table, or column aliases from any level of the query,
depending on minor details of the plan tree.  You could even get totally
empty field names, which is disastrous for cases like row_to_json().

It seems unwise to change this behavior too much in stable branches,
however, since users might not have noticed that they weren't getting
the least-unintuitive choice of field names.  Therefore, in the back
branches, only change the results when the child plan has returned an
actually-empty field name.  (We assume that can't happen with a named
rowtype, so this also dodges the issue of possibly producing RECORD-typed
output from a Var with a named composite result type.)  As in the sister
patch for HEAD, we can get a better name to use from the Var's
corresponding RTE.  There is no need to touch the RowExpr code since it
was already using a copy of the RTE's alias list for RECORD cases.

Back-patch as far as 9.2.  Before that we did not have row_to_json()
so there were no core functions potentially affected by bogus field
names.  While 9.1 and earlier do have contrib's hstore(record) which
is also affected, those versions don't seem to produce empty field names
(at least not in the known problem cases), so we'll leave them alone.

src/backend/executor/execQual.c
src/backend/executor/execTuples.c
src/include/executor/executor.h
src/include/nodes/execnodes.h
src/test/regress/expected/rowtypes.out
src/test/regress/sql/rowtypes.sql

index 229c2b051b2aa269afa1ead3228da107437031f1..dae8bf0f2c1c251d465beb907d82531f592cfdc5 100644 (file)
@@ -50,6 +50,7 @@
 #include "nodes/nodeFuncs.h"
 #include "optimizer/planner.h"
 #include "parser/parse_coerce.h"
+#include "parser/parsetree.h"
 #include "pgstat.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
@@ -712,6 +713,8 @@ ExecEvalWholeRowVar(WholeRowVarExprState *wrvstate, ExprContext *econtext,
 {
    Var        *variable = (Var *) wrvstate->xprstate.expr;
    TupleTableSlot *slot;
+   TupleDesc   output_tupdesc;
+   MemoryContext oldcontext;
    bool        needslow = false;
 
    if (isDone)
@@ -787,8 +790,6 @@ ExecEvalWholeRowVar(WholeRowVarExprState *wrvstate, ExprContext *econtext,
            /* If so, build the junkfilter in the query memory context */
            if (junk_filter_needed)
            {
-               MemoryContext oldcontext;
-
                oldcontext = MemoryContextSwitchTo(econtext->ecxt_per_query_memory);
                wrvstate->wrv_junkFilter =
                    ExecInitJunkFilter(subplan->plan->targetlist,
@@ -860,10 +861,61 @@ ExecEvalWholeRowVar(WholeRowVarExprState *wrvstate, ExprContext *econtext,
                needslow = true;    /* need runtime check for null */
        }
 
+       /*
+        * Use the variable's declared rowtype as the descriptor for the
+        * output values.  In particular, we *must* absorb any attisdropped
+        * markings.
+        */
+       oldcontext = MemoryContextSwitchTo(econtext->ecxt_per_query_memory);
+       output_tupdesc = CreateTupleDescCopy(var_tupdesc);
+       MemoryContextSwitchTo(oldcontext);
+
        ReleaseTupleDesc(var_tupdesc);
    }
+   else
+   {
+       /*
+        * In the RECORD case, we use the input slot's rowtype as the
+        * descriptor for the output values, modulo possibly assigning new
+        * column names below.
+        */
+       oldcontext = MemoryContextSwitchTo(econtext->ecxt_per_query_memory);
+       output_tupdesc = CreateTupleDescCopy(slot->tts_tupleDescriptor);
+       MemoryContextSwitchTo(oldcontext);
+   }
 
-   /* Skip the checking on future executions of node */
+   /*
+    * Construct a tuple descriptor for the composite values we'll produce,
+    * and make sure its record type is "blessed".  The main reason to do this
+    * is to be sure that operations such as row_to_json() will see the
+    * desired column names when they look up the descriptor from the type
+    * information embedded in the composite values.
+    *
+    * We already got the correct physical datatype info above, but now we
+    * should try to find the source RTE and adopt its column aliases, in case
+    * they are different from the original rowtype's names.  But to minimize
+    * compatibility breakage, don't do this for Vars of named composite
+    * types, only for Vars of type RECORD.
+    *
+    * If we can't locate the RTE, assume the column names we've got are OK.
+    * (As of this writing, the only cases where we can't locate the RTE are
+    * in execution of trigger WHEN clauses, and then the Var will have the
+    * trigger's relation's rowtype, so its names are fine.)
+    */
+   if (variable->vartype == RECORDOID &&
+       econtext->ecxt_estate &&
+       variable->varno <= list_length(econtext->ecxt_estate->es_range_table))
+   {
+       RangeTblEntry *rte = rt_fetch(variable->varno,
+                                     econtext->ecxt_estate->es_range_table);
+
+       ExecTypeSetColNames(output_tupdesc, rte->eref->colnames);
+   }
+
+   /* Bless the tupdesc if needed, and save it in the execution state */
+   wrvstate->wrv_tupdesc = BlessTupleDesc(output_tupdesc);
+
+   /* Skip all the above on future executions of node */
    if (needslow)
        wrvstate->xprstate.evalfunc = (ExprStateEvalFunc) ExecEvalWholeRowSlow;
    else
@@ -886,7 +938,6 @@ ExecEvalWholeRowFast(WholeRowVarExprState *wrvstate, ExprContext *econtext,
 {
    Var        *variable = (Var *) wrvstate->xprstate.expr;
    TupleTableSlot *slot;
-   TupleDesc   slot_tupdesc;
    HeapTupleHeader dtuple;
 
    if (isDone)
@@ -916,34 +967,16 @@ ExecEvalWholeRowFast(WholeRowVarExprState *wrvstate, ExprContext *econtext,
    if (wrvstate->wrv_junkFilter != NULL)
        slot = ExecFilterJunk(wrvstate->wrv_junkFilter, slot);
 
-   /*
-    * If it's a RECORD Var, we'll use the slot's type ID info.  It's likely
-    * that the slot's type is also RECORD; if so, make sure it's been
-    * "blessed", so that the Datum can be interpreted later.  (Note: we must
-    * do this here, not in ExecEvalWholeRowVar, because some plan trees may
-    * return different slots at different times.  We have to be ready to
-    * bless additional slots during the run.)
-    */
-   slot_tupdesc = slot->tts_tupleDescriptor;
-   if (variable->vartype == RECORDOID &&
-       slot_tupdesc->tdtypeid == RECORDOID &&
-       slot_tupdesc->tdtypmod < 0)
-       assign_record_type_typmod(slot_tupdesc);
-
    /*
     * Copy the slot tuple and make sure any toasted fields get detoasted.
     */
    dtuple = DatumGetHeapTupleHeader(ExecFetchSlotTupleDatum(slot));
 
    /*
-    * If the Var identifies a named composite type, label the datum with that
-    * type; otherwise we'll use the slot's info.
+    * Label the datum with the composite type info we identified before.
     */
-   if (variable->vartype != RECORDOID)
-   {
-       HeapTupleHeaderSetTypeId(dtuple, variable->vartype);
-       HeapTupleHeaderSetTypMod(dtuple, variable->vartypmod);
-   }
+   HeapTupleHeaderSetTypeId(dtuple, wrvstate->wrv_tupdesc->tdtypeid);
+   HeapTupleHeaderSetTypMod(dtuple, wrvstate->wrv_tupdesc->tdtypmod);
 
    return PointerGetDatum(dtuple);
 }
@@ -997,8 +1030,9 @@ ExecEvalWholeRowSlow(WholeRowVarExprState *wrvstate, ExprContext *econtext,
    tuple = ExecFetchSlotTuple(slot);
    tupleDesc = slot->tts_tupleDescriptor;
 
+   /* wrv_tupdesc is a good enough representation of the Var's rowtype */
    Assert(variable->vartype != RECORDOID);
-   var_tupdesc = lookup_rowtype_tupdesc(variable->vartype, -1);
+   var_tupdesc = wrvstate->wrv_tupdesc;
 
    /* Check to see if any dropped attributes are non-null */
    for (i = 0; i < var_tupdesc->natts; i++)
@@ -1025,12 +1059,10 @@ ExecEvalWholeRowSlow(WholeRowVarExprState *wrvstate, ExprContext *econtext,
    dtuple = DatumGetHeapTupleHeader(ExecFetchSlotTupleDatum(slot));
 
    /*
-    * Reset datum's type ID fields to match the Var.
+    * Label the datum with the composite type info we identified before.
     */
-   HeapTupleHeaderSetTypeId(dtuple, variable->vartype);
-   HeapTupleHeaderSetTypMod(dtuple, variable->vartypmod);
-
-   ReleaseTupleDesc(var_tupdesc);
+   HeapTupleHeaderSetTypeId(dtuple, wrvstate->wrv_tupdesc->tdtypeid);
+   HeapTupleHeaderSetTypMod(dtuple, wrvstate->wrv_tupdesc->tdtypmod);
 
    return PointerGetDatum(dtuple);
 }
@@ -4375,6 +4407,7 @@ ExecInitExpr(Expr *node, PlanState *parent)
                WholeRowVarExprState *wstate = makeNode(WholeRowVarExprState);
 
                wstate->parent = parent;
+               wstate->wrv_tupdesc = NULL;
                wstate->wrv_junkFilter = NULL;
                state = (ExprState *) wstate;
                state->evalfunc = (ExprStateEvalFunc) ExecEvalWholeRowVar;
index 43d047747f1282a354964ed858f1c82aa31afdee..c9a3de894d8cbfaa9cfe58b36e120a5f034b9c8b 100644 (file)
@@ -984,6 +984,49 @@ ExecTypeFromExprList(List *exprList, List *namesList)
    return typeInfo;
 }
 
+/*
+ * ExecTypeSetColNames - set column names in a TupleDesc
+ *
+ * Column names must be provided as an alias list (list of String nodes).
+ * We set names only in TupleDesc columns that lacked one before.
+ */
+void
+ExecTypeSetColNames(TupleDesc typeInfo, List *namesList)
+{
+   bool        modified = false;
+   int         colno = 0;
+   ListCell   *lc;
+
+   foreach(lc, namesList)
+   {
+       char       *cname = strVal(lfirst(lc));
+       Form_pg_attribute attr;
+
+       /* Guard against too-long names list */
+       if (colno >= typeInfo->natts)
+           break;
+       attr = typeInfo->attrs[colno++];
+
+       /* Ignore empty aliases (these must be for dropped columns) */
+       if (cname[0] == '\0')
+           continue;
+
+       /* Change tupdesc only if we didn't have a name before */
+       if (NameStr(attr->attname)[0] == '\0')
+       {
+           namestrcpy(&(attr->attname), cname);
+           modified = true;
+       }
+   }
+
+   /* If we modified the tupdesc, it's now a new record type */
+   if (modified)
+   {
+       typeInfo->tdtypeid = RECORDOID;
+       typeInfo->tdtypmod = -1;
+   }
+}
+
 /*
  * BlessTupleDesc - make a completed tuple descriptor useful for SRFs
  *
index 4043b8f4483816d78d7a2da41150b4616ef86586..2c69db773a66df40c98d4370d1306e78c594b235 100644 (file)
@@ -264,6 +264,7 @@ extern TupleTableSlot *ExecInitNullTupleSlot(EState *estate,
 extern TupleDesc ExecTypeFromTL(List *targetList, bool hasoid);
 extern TupleDesc ExecCleanTypeFromTL(List *targetList, bool hasoid);
 extern TupleDesc ExecTypeFromExprList(List *exprList, List *namesList);
+extern void ExecTypeSetColNames(TupleDesc typeInfo, List *namesList);
 extern void UpdateChangedParamSet(PlanState *node, Bitmapset *newchg);
 
 typedef struct TupOutputState
index c443112d806ad2b94b9ab9f3101a7ff815c433b2..0b2ad80fac9d0f4e6072de73e64cc195db0d6c59 100644 (file)
@@ -574,6 +574,7 @@ typedef struct WholeRowVarExprState
    ExprState   xprstate;
    struct PlanState *parent;   /* parent PlanState, or NULL if none */
    JunkFilter *wrv_junkFilter; /* JunkFilter to remove resjunk cols */
+   TupleDesc   wrv_tupdesc;    /* descriptor for resulting tuples */
 } WholeRowVarExprState;
 
 /* ----------------
index 88e7bfab84232363453845e45b4ad65bd13be167..d7f1dd5336f1ba52c294a7767d8e5081dc9d6ee1 100644 (file)
@@ -474,3 +474,163 @@ select (row('Jim', 'Beam')).text;  -- error
 ERROR:  could not identify column "text" in record data type
 LINE 1: select (row('Jim', 'Beam')).text;
                 ^
+--
+-- Test that composite values are seen to have the correct column names
+-- (bug #11210 and other reports)
+--
+select row_to_json(i) from int8_tbl i;
+                  row_to_json                   
+------------------------------------------------
+ {"q1":123,"q2":456}
+ {"q1":123,"q2":4567890123456789}
+ {"q1":4567890123456789,"q2":123}
+ {"q1":4567890123456789,"q2":4567890123456789}
+ {"q1":4567890123456789,"q2":-4567890123456789}
+(5 rows)
+
+select row_to_json(i) from int8_tbl i(x,y);
+                  row_to_json                   
+------------------------------------------------
+ {"q1":123,"q2":456}
+ {"q1":123,"q2":4567890123456789}
+ {"q1":4567890123456789,"q2":123}
+ {"q1":4567890123456789,"q2":4567890123456789}
+ {"q1":4567890123456789,"q2":-4567890123456789}
+(5 rows)
+
+create temp view vv1 as select * from int8_tbl;
+select row_to_json(i) from vv1 i;
+                  row_to_json                   
+------------------------------------------------
+ {"q1":123,"q2":456}
+ {"q1":123,"q2":4567890123456789}
+ {"q1":4567890123456789,"q2":123}
+ {"q1":4567890123456789,"q2":4567890123456789}
+ {"q1":4567890123456789,"q2":-4567890123456789}
+(5 rows)
+
+select row_to_json(i) from vv1 i(x,y);
+                  row_to_json                   
+------------------------------------------------
+ {"q1":123,"q2":456}
+ {"q1":123,"q2":4567890123456789}
+ {"q1":4567890123456789,"q2":123}
+ {"q1":4567890123456789,"q2":4567890123456789}
+ {"q1":4567890123456789,"q2":-4567890123456789}
+(5 rows)
+
+select row_to_json(ss) from
+  (select q1, q2 from int8_tbl) as ss;
+                  row_to_json                   
+------------------------------------------------
+ {"q1":123,"q2":456}
+ {"q1":123,"q2":4567890123456789}
+ {"q1":4567890123456789,"q2":123}
+ {"q1":4567890123456789,"q2":4567890123456789}
+ {"q1":4567890123456789,"q2":-4567890123456789}
+(5 rows)
+
+select row_to_json(ss) from
+  (select q1, q2 from int8_tbl offset 0) as ss;
+                  row_to_json                   
+------------------------------------------------
+ {"q1":123,"q2":456}
+ {"q1":123,"q2":4567890123456789}
+ {"q1":4567890123456789,"q2":123}
+ {"q1":4567890123456789,"q2":4567890123456789}
+ {"q1":4567890123456789,"q2":-4567890123456789}
+(5 rows)
+
+select row_to_json(ss) from
+  (select q1 as a, q2 as b from int8_tbl) as ss;
+                 row_to_json                  
+----------------------------------------------
+ {"a":123,"b":456}
+ {"a":123,"b":4567890123456789}
+ {"a":4567890123456789,"b":123}
+ {"a":4567890123456789,"b":4567890123456789}
+ {"a":4567890123456789,"b":-4567890123456789}
+(5 rows)
+
+select row_to_json(ss) from
+  (select q1 as a, q2 as b from int8_tbl offset 0) as ss;
+                  row_to_json                   
+------------------------------------------------
+ {"q1":123,"q2":456}
+ {"q1":123,"q2":4567890123456789}
+ {"q1":4567890123456789,"q2":123}
+ {"q1":4567890123456789,"q2":4567890123456789}
+ {"q1":4567890123456789,"q2":-4567890123456789}
+(5 rows)
+
+select row_to_json(ss) from
+  (select q1 as a, q2 as b from int8_tbl) as ss(x,y);
+                 row_to_json                  
+----------------------------------------------
+ {"x":123,"y":456}
+ {"x":123,"y":4567890123456789}
+ {"x":4567890123456789,"y":123}
+ {"x":4567890123456789,"y":4567890123456789}
+ {"x":4567890123456789,"y":-4567890123456789}
+(5 rows)
+
+select row_to_json(ss) from
+  (select q1 as a, q2 as b from int8_tbl offset 0) as ss(x,y);
+                  row_to_json                   
+------------------------------------------------
+ {"q1":123,"q2":456}
+ {"q1":123,"q2":4567890123456789}
+ {"q1":4567890123456789,"q2":123}
+ {"q1":4567890123456789,"q2":4567890123456789}
+ {"q1":4567890123456789,"q2":-4567890123456789}
+(5 rows)
+
+explain (costs off)
+select row_to_json(q) from
+  (select thousand, tenthous from tenk1
+   where thousand = 42 and tenthous < 2000 offset 0) q;
+                         QUERY PLAN                          
+-------------------------------------------------------------
+ Subquery Scan on q
+   ->  Index Only Scan using tenk1_thous_tenthous on tenk1
+         Index Cond: ((thousand = 42) AND (tenthous < 2000))
+(3 rows)
+
+select row_to_json(q) from
+  (select thousand, tenthous from tenk1
+   where thousand = 42 and tenthous < 2000 offset 0) q;
+           row_to_json           
+---------------------------------
+ {"thousand":42,"tenthous":42}
+ {"thousand":42,"tenthous":1042}
+(2 rows)
+
+select row_to_json(q) from
+  (select thousand as x, tenthous as y from tenk1
+   where thousand = 42 and tenthous < 2000 offset 0) q;
+    row_to_json    
+-------------------
+ {"x":42,"y":42}
+ {"x":42,"y":1042}
+(2 rows)
+
+select row_to_json(q) from
+  (select thousand as x, tenthous as y from tenk1
+   where thousand = 42 and tenthous < 2000 offset 0) q(a,b);
+    row_to_json    
+-------------------
+ {"a":42,"b":42}
+ {"a":42,"b":1042}
+(2 rows)
+
+create temp table tt1 as select * from int8_tbl limit 2;
+create temp table tt2 () inherits(tt1);
+insert into tt2 values(0,0);
+select row_to_json(r) from (select q2,q1 from tt1 offset 0) r;
+           row_to_json            
+----------------------------------
+ {"q2":456,"q1":123}
+ {"q2":4567890123456789,"q1":123}
+ {"q2":0,"q1":0}
+(3 rows)
+
index 65ebdc566abf455187096439ef5c9c031a600257..bc3f02102003400b50990c288b9d163b2db01efb 100644 (file)
@@ -227,3 +227,47 @@ select cast (row('Jim', 'Beam') as text);
 select (row('Jim', 'Beam'))::text;
 select text(row('Jim', 'Beam'));  -- error
 select (row('Jim', 'Beam')).text;  -- error
+
+--
+-- Test that composite values are seen to have the correct column names
+-- (bug #11210 and other reports)
+--
+
+select row_to_json(i) from int8_tbl i;
+select row_to_json(i) from int8_tbl i(x,y);
+
+create temp view vv1 as select * from int8_tbl;
+select row_to_json(i) from vv1 i;
+select row_to_json(i) from vv1 i(x,y);
+
+select row_to_json(ss) from
+  (select q1, q2 from int8_tbl) as ss;
+select row_to_json(ss) from
+  (select q1, q2 from int8_tbl offset 0) as ss;
+select row_to_json(ss) from
+  (select q1 as a, q2 as b from int8_tbl) as ss;
+select row_to_json(ss) from
+  (select q1 as a, q2 as b from int8_tbl offset 0) as ss;
+select row_to_json(ss) from
+  (select q1 as a, q2 as b from int8_tbl) as ss(x,y);
+select row_to_json(ss) from
+  (select q1 as a, q2 as b from int8_tbl offset 0) as ss(x,y);
+
+explain (costs off)
+select row_to_json(q) from
+  (select thousand, tenthous from tenk1
+   where thousand = 42 and tenthous < 2000 offset 0) q;
+select row_to_json(q) from
+  (select thousand, tenthous from tenk1
+   where thousand = 42 and tenthous < 2000 offset 0) q;
+select row_to_json(q) from
+  (select thousand as x, tenthous as y from tenk1
+   where thousand = 42 and tenthous < 2000 offset 0) q;
+select row_to_json(q) from
+  (select thousand as x, tenthous as y from tenk1
+   where thousand = 42 and tenthous < 2000 offset 0) q(a,b);
+
+create temp table tt1 as select * from int8_tbl limit 2;
+create temp table tt2 () inherits(tt1);
+insert into tt2 values(0,0);
+select row_to_json(r) from (select q2,q1 from tt1 offset 0) r;