From 4d1cc1fc3c6c7ed13418bee9032825b488650151 Mon Sep 17 00:00:00 2001
From: Julien Rouhaud <julien.rouhaud@free.fr>
Date: Sat, 4 Jun 2022 03:07:47 +0800
Subject: [PATCH v1 2/5] WIP Handle logical attnum

A new attnum column is added to pg_attribute, to store the logical column
position.  The default is to have the same as the physical position.

A relation is now expanded in logical order, for instance:

- A plain SELECT * FROM table
- A RETURNING * clause in a DML command
- A COPY table FROM/TO without explicit column names

Note that there's for now no possibility to declare a different logical
position.

psql displays a table columns information using the logical order rather the
physical order, and if verbose emits an addition "Physical order" footer if the
logical layout is different from the physical one.

Many problems that still exist, for instance deparsing view that didn't have
explicit column list and later requires it (e.g. after renaming attributes).

Author: Julien Rouhaud
Reviewed-by: FIXME
Discussion: FIXME
---
 src/backend/access/common/attmap.c     |   5 +-
 src/backend/access/common/tupconvert.c |   7 +-
 src/backend/access/common/tupdesc.c    |  43 +++++++++++
 src/backend/bootstrap/bootstrap.c      |   1 +
 src/backend/catalog/genbki.pl          |   2 +
 src/backend/catalog/heap.c             |   8 ++
 src/backend/catalog/index.c            |   1 +
 src/backend/commands/copy.c            |  48 ++++++++++--
 src/backend/commands/tablecmds.c       |  11 ++-
 src/backend/nodes/copyfuncs.c          |   2 +
 src/backend/nodes/equalfuncs.c         |   1 +
 src/backend/nodes/outfuncs.c           |   2 +
 src/backend/nodes/readfuncs.c          |   2 +
 src/backend/optimizer/util/plancat.c   |   2 +
 src/backend/optimizer/util/tlist.c     |  43 +++++++++++
 src/backend/parser/analyze.c           |   2 +-
 src/backend/parser/parse_clause.c      |   1 +
 src/backend/parser/parse_relation.c    | 100 ++++++++++++++++++++-----
 src/backend/parser/parse_target.c      | 100 ++++++++++++++++++-------
 src/backend/parser/parse_utilcmd.c     |   9 ++-
 src/backend/utils/cache/relcache.c     |   5 +-
 src/bin/psql/describe.c                |  71 +++++++++++++++---
 src/include/access/tupdesc.h           |   2 +
 src/include/catalog/pg_attribute.h     |   6 ++
 src/include/nodes/primnodes.h          |   4 +-
 src/include/optimizer/tlist.h          |   1 +
 src/include/parser/parse_node.h        |   3 +-
 src/include/parser/parse_relation.h    |   2 +-
 src/test/regress/regress.c             |  17 ++++-
 29 files changed, 424 insertions(+), 77 deletions(-)

diff --git a/src/backend/access/common/attmap.c b/src/backend/access/common/attmap.c
index d84b1bcdc4..f323b4a72e 100644
--- a/src/backend/access/common/attmap.c
+++ b/src/backend/access/common/attmap.c
@@ -100,6 +100,7 @@ build_attrmap_by_position(TupleDesc indesc,
 		Form_pg_attribute att = TupleDescAttr(outdesc, i);
 		Oid			atttypid;
 		int32		atttypmod;
+		AttrNumber attphysnum = att->attphysnum;
 
 		if (att->attisdropped)
 			continue;			/* attrMap->attnums[i] is already 0 */
@@ -125,11 +126,11 @@ build_attrmap_by_position(TupleDesc indesc,
 								   format_type_with_typemod(atttypid,
 															atttypmod),
 								   noutcols)));
-			attrMap->attnums[i] = (AttrNumber) (j + 1);
+			attrMap->attnums[attphysnum - 1] = (AttrNumber) (j + 1);
 			j++;
 			break;
 		}
-		if (attrMap->attnums[i] == 0)
+		if (attrMap->attnums[attphysnum - 1] == 0)
 			same = false;		/* we'll complain below */
 	}
 
diff --git a/src/backend/access/common/tupconvert.c b/src/backend/access/common/tupconvert.c
index 4010e20cfb..68d05f343a 100644
--- a/src/backend/access/common/tupconvert.c
+++ b/src/backend/access/common/tupconvert.c
@@ -60,12 +60,15 @@ convert_tuples_by_position(TupleDesc indesc,
 						   TupleDesc outdesc,
 						   const char *msg)
 {
+	TupleDesc sorted_outdesc = CreateTupleDescCopy(outdesc);
 	TupleConversionMap *map;
 	int			n;
 	AttrMap    *attrMap;
 
-	/* Verify compatibility and prepare attribute-number map */
-	attrMap = build_attrmap_by_position(indesc, outdesc, msg);
+	/* Verify compatibility and prepare attribute-number map. */
+	TupleDescSortByAttnum(sorted_outdesc);
+	attrMap = build_attrmap_by_position(indesc, sorted_outdesc, msg);
+	pfree(sorted_outdesc);
 
 	if (attrMap == NULL)
 	{
diff --git a/src/backend/access/common/tupdesc.c b/src/backend/access/common/tupdesc.c
index 409a93ccdf..e9f9afbd0c 100644
--- a/src/backend/access/common/tupdesc.c
+++ b/src/backend/access/common/tupdesc.c
@@ -128,6 +128,9 @@ CreateTupleDescCopy(TupleDesc tupdesc)
 	{
 		Form_pg_attribute att = TupleDescAttr(desc, i);
 
+		/* Caller should have provided it */
+		Assert(AttributeNumberIsValid(att->attnum));
+
 		att->attnotnull = false;
 		att->atthasdef = false;
 		att->atthasmissing = false;
@@ -285,6 +288,8 @@ TupleDescCopyEntry(TupleDesc dst, AttrNumber dstAttno,
 	/*
 	 * Aside from updating the attno, we'd better reset attcacheoff.
 	 *
+	 * The attnum should however be preserved.
+	 *
 	 * XXX Actually, to be entirely safe we'd need to reset the attcacheoff of
 	 * all following columns in dst as well.  Current usage scenarios don't
 	 * require that though, because all following columns will get initialized
@@ -620,6 +625,8 @@ TupleDescInitEntry(TupleDesc desc,
 	att->atttypmod = typmod;
 
 	att->attphysnum = attributeNumber;
+	/* FIXME should we put 0 and let caller always set it as needed? */
+	att->attnum = attributeNumber;
 	att->attndims = attdim;
 
 	att->attnotnull = false;
@@ -765,6 +772,40 @@ TupleDescInitEntryCollation(TupleDesc desc,
 	TupleDescAttr(desc, attributeNumber - 1)->attcollation = collationid;
 }
 
+/*
+ * cmp_attnum
+ *
+ * Comparator for sorting TupleDesc attributes by they logical position
+ * (attnum).
+ */
+static int
+cmp_attnum(const void *a, const void *b)
+{
+	FormData_pg_attribute *atta = (FormData_pg_attribute *) a;
+	FormData_pg_attribute *attb = (FormData_pg_attribute *) b;
+
+	return (atta->attnum > attb->attnum) ? 1
+		: (atta->attnum == attb->attnum) ? 0
+		: -1;
+}
+
+/*
+ * TupleDescSortByAttnum
+ *
+ * Sort the attributes by their logical position (attnum) rather than their
+ * physical position (attphysnum).
+ * This function will modify the given TupleDesc, so caller should make a copy
+ * first if needed.
+ */
+void
+TupleDescSortByAttnum(TupleDesc desc)
+{
+	if (desc->natts <= 1)
+		return;
+
+	qsort(desc->attrs, desc->natts, sizeof(FormData_pg_attribute), cmp_attnum);
+}
+
 
 /*
  * BuildDescForRelation
@@ -828,6 +869,8 @@ BuildDescForRelation(List *schema)
 		TupleDescInitEntry(desc, attphysnum, attname,
 						   atttypid, atttypmod, attdim);
 		att = TupleDescAttr(desc, attphysnum - 1);
+		/* FIXME - change me when there's a syntax to specify it */
+		att->attnum = attphysnum;
 
 		/* Override TupleDescInitEntry's settings as requested */
 		TupleDescInitEntryCollation(desc, attphysnum, attcollation);
diff --git a/src/backend/bootstrap/bootstrap.c b/src/backend/bootstrap/bootstrap.c
index 37107dc12b..767bdd5477 100644
--- a/src/backend/bootstrap/bootstrap.c
+++ b/src/backend/bootstrap/bootstrap.c
@@ -518,6 +518,7 @@ DefineAttr(char *name, char *type, int attphysnum, int nullness)
 	namestrcpy(&attrtypes[attphysnum]->attname, name);
 	elog(DEBUG4, "column %s %s", NameStr(attrtypes[attphysnum]->attname), type);
 	attrtypes[attphysnum]->attphysnum = attphysnum + 1;
+	attrtypes[attphysnum]->attnum = attphysnum + 1;
 
 	typeoid = gettype(type);
 
diff --git a/src/backend/catalog/genbki.pl b/src/backend/catalog/genbki.pl
index f1d33dd949..cc23c5e909 100644
--- a/src/backend/catalog/genbki.pl
+++ b/src/backend/catalog/genbki.pl
@@ -852,6 +852,7 @@ sub gen_pg_attribute
 			$attphysnum++;
 			my %row;
 			$row{attphysnum}   = $attphysnum;
+			$row{attnum}   = $attphysnum;
 			$row{attrelid} = $table->{relation_oid};
 
 			morph_row_for_pgattr(\%row, $schema, $attr, $priorfixedwidth);
@@ -888,6 +889,7 @@ sub gen_pg_attribute
 				$attphysnum--;
 				my %row;
 				$row{attphysnum}        = $attphysnum;
+				$row{attnum}        = $attphysnum;
 				$row{attrelid}      = $table->{relation_oid};
 				$row{attstattarget} = '0';
 
diff --git a/src/backend/catalog/heap.c b/src/backend/catalog/heap.c
index ed9929e67a..1ce0517e57 100644
--- a/src/backend/catalog/heap.c
+++ b/src/backend/catalog/heap.c
@@ -732,6 +732,14 @@ InsertPgAttributeTuples(Relation pg_attribute_rel,
 		slot[slotCount]->tts_values[Anum_pg_attribute_attstattarget - 1] = Int32GetDatum(attrs->attstattarget);
 		slot[slotCount]->tts_values[Anum_pg_attribute_attlen - 1] = Int16GetDatum(attrs->attlen);
 		slot[slotCount]->tts_values[Anum_pg_attribute_attphysnum - 1] = Int16GetDatum(attrs->attphysnum);
+		/*
+		 * If caller provided a specific attnum use it, otherwise just fallback
+		 * on attphysnum
+		 */
+		if (AttributeNumberIsValid(attrs->attnum))
+			slot[slotCount]->tts_values[Anum_pg_attribute_attnum - 1] = Int16GetDatum(attrs->attnum);
+		else
+			slot[slotCount]->tts_values[Anum_pg_attribute_attnum - 1] = Int16GetDatum(attrs->attphysnum);
 		slot[slotCount]->tts_values[Anum_pg_attribute_attndims - 1] = Int32GetDatum(attrs->attndims);
 		slot[slotCount]->tts_values[Anum_pg_attribute_attcacheoff - 1] = Int32GetDatum(-1);
 		slot[slotCount]->tts_values[Anum_pg_attribute_atttypmod - 1] = Int32GetDatum(attrs->atttypmod);
diff --git a/src/backend/catalog/index.c b/src/backend/catalog/index.c
index c514e3b622..153916e44c 100644
--- a/src/backend/catalog/index.c
+++ b/src/backend/catalog/index.c
@@ -311,6 +311,7 @@ ConstructTupleDescriptor(Relation heapRelation,
 
 		MemSet(to, 0, ATTRIBUTE_FIXED_PART_SIZE);
 		to->attphysnum = i + 1;
+		to->attnum = to->attphysnum;
 		to->attstattarget = -1;
 		to->attcacheoff = -1;
 		to->attislocal = true;
diff --git a/src/backend/commands/copy.c b/src/backend/commands/copy.c
index 6b15837dba..7ee0703ed0 100644
--- a/src/backend/commands/copy.c
+++ b/src/backend/commands/copy.c
@@ -65,6 +65,7 @@ DoCopy(ParseState *pstate, const CopyStmt *stmt,
 	   int stmt_location, int stmt_len,
 	   uint64 *processed)
 {
+	List	   *attlist = stmt->attlist;
 	bool		is_from = stmt->is_from;
 	bool		pipe = (stmt->filename == NULL);
 	Relation	rel;
@@ -147,7 +148,34 @@ DoCopy(ParseState *pstate, const CopyStmt *stmt,
 		}
 
 		tupDesc = RelationGetDescr(rel);
-		attnums = CopyGetAttnums(tupDesc, rel, stmt->attlist);
+
+		/* If no column list has been provided, generate one using the logical
+		 * attribute numbers rather than physical order.
+		 * We don't include generated columns in the generated full list and we
+		 * don't allow them to be specified explicitly.
+		 */
+		if (attlist == NIL)
+		{
+			TupleDesc tmp = CreateTupleDescCopyConstr(tupDesc);
+			TupleDescSortByAttnum(tmp);
+
+			for (int i = 0; i < tmp->natts; i++)
+			{
+				Form_pg_attribute attr = TupleDescAttr(tmp, i);
+
+				if (attr->attisdropped)
+					continue;
+				if (attr->attgenerated)
+					continue;
+
+				attlist = lappend(attlist,
+										makeString(pstrdup(NameStr(attr->attname))));
+			}
+
+			pfree(tmp);
+		}
+
+		attnums = CopyGetAttnums(tupDesc, rel, attlist);
 		foreach(cur, attnums)
 		{
 			int			attno = lfirst_int(cur) -
@@ -199,8 +227,10 @@ DoCopy(ParseState *pstate, const CopyStmt *stmt,
 			 * In the case that columns are specified in the attribute list,
 			 * create a ColumnRef and ResTarget for each column and add them
 			 * to the target list for the resulting SELECT statement.
+			 *
+			 * FIXME - should be dead code
 			 */
-			if (!stmt->attlist)
+			if (!attlist)
 			{
 				cr = makeNode(ColumnRef);
 				cr->fields = list_make1(makeNode(A_Star));
@@ -218,7 +248,7 @@ DoCopy(ParseState *pstate, const CopyStmt *stmt,
 			{
 				ListCell   *lc;
 
-				foreach(lc, stmt->attlist)
+				foreach(lc, attlist)
 				{
 					/*
 					 * Build the ColumnRef for each column.  The ColumnRef
@@ -294,7 +324,7 @@ DoCopy(ParseState *pstate, const CopyStmt *stmt,
 
 		cstate = BeginCopyFrom(pstate, rel, whereClause,
 							   stmt->filename, stmt->is_program,
-							   NULL, stmt->attlist, stmt->options);
+							   NULL, attlist, stmt->options);
 		*processed = CopyFrom(cstate);	/* copy from file to database */
 		EndCopyFrom(cstate);
 	}
@@ -304,7 +334,7 @@ DoCopy(ParseState *pstate, const CopyStmt *stmt,
 
 		cstate = BeginCopyTo(pstate, rel, query, relid,
 							 stmt->filename, stmt->is_program,
-							 stmt->attlist, stmt->options);
+							 attlist, stmt->options);
 		*processed = DoCopyTo(cstate);	/* copy from database to file */
 		EndCopyTo(cstate);
 	}
@@ -724,11 +754,13 @@ CopyGetAttnums(TupleDesc tupDesc, Relation rel, List *attnamelist)
 
 		for (i = 0; i < attr_count; i++)
 		{
-			if (TupleDescAttr(tupDesc, i)->attisdropped)
+			Form_pg_attribute attr = TupleDescAttr(tupDesc, i);
+
+			if (attr->attisdropped)
 				continue;
-			if (TupleDescAttr(tupDesc, i)->attgenerated)
+			if (attr->attgenerated)
 				continue;
-			attnums = lappend_int(attnums, i + 1);
+			attnums = lappend_int(attnums, attr->attphysnum);
 		}
 	}
 	else
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index ff9a9fc911..007e355d9d 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -2504,7 +2504,14 @@ MergeAttributes(List *schema, List *supers, char relpersistence,
 			aclcheck_error(ACLCHECK_NOT_OWNER, get_relkind_objtype(relation->rd_rel->relkind),
 						   RelationGetRelationName(relation));
 
-		tupleDesc = RelationGetDescr(relation);
+		/*
+		 * Make a copy of the tupledesc and sort it by logical attnum, as this
+		 * is the expected order for inherited relations.
+		 * FIXME - should keep the original attphysnum and compute an attnum
+		 * instead?
+		 */
+		tupleDesc = CreateTupleDescCopyConstr(RelationGetDescr(relation));
+		TupleDescSortByAttnum(tupleDesc);
 		constr = tupleDesc->constr;
 
 		/*
@@ -6815,6 +6822,8 @@ ATExecAddColumn(List **wqueue, AlteredTableInfo *tab, Relation rel,
 	attribute.attstattarget = (newattnum > 0) ? -1 : 0;
 	attribute.attlen = tform->typlen;
 	attribute.attphysnum = newattnum;
+	/* FIXME - change me when there's a syntax to specify it */
+	attribute.attnum = newattnum;
 	attribute.attndims = list_length(colDef->typeName->arrayBounds);
 	attribute.atttypmod = typmod;
 	attribute.attbyval = tform->typbyval;
diff --git a/src/backend/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c
index 51d630fa89..68c6fb7c93 100644
--- a/src/backend/nodes/copyfuncs.c
+++ b/src/backend/nodes/copyfuncs.c
@@ -1455,6 +1455,7 @@ _copyVar(const Var *from)
 
 	COPY_SCALAR_FIELD(varno);
 	COPY_SCALAR_FIELD(varattno);
+	COPY_SCALAR_FIELD(varnum);
 	COPY_SCALAR_FIELD(vartype);
 	COPY_SCALAR_FIELD(vartypmod);
 	COPY_SCALAR_FIELD(varcollid);
@@ -1814,6 +1815,7 @@ _copyFieldSelect(const FieldSelect *from)
 
 	COPY_NODE_FIELD(arg);
 	COPY_SCALAR_FIELD(fieldnum);
+	COPY_SCALAR_FIELD(fieldlognum);
 	COPY_SCALAR_FIELD(resulttype);
 	COPY_SCALAR_FIELD(resulttypmod);
 	COPY_SCALAR_FIELD(resultcollid);
diff --git a/src/backend/nodes/equalfuncs.c b/src/backend/nodes/equalfuncs.c
index e747e1667d..d9ca25606b 100644
--- a/src/backend/nodes/equalfuncs.c
+++ b/src/backend/nodes/equalfuncs.c
@@ -551,6 +551,7 @@ _equalFieldSelect(const FieldSelect *a, const FieldSelect *b)
 {
 	COMPARE_NODE_FIELD(arg);
 	COMPARE_SCALAR_FIELD(fieldnum);
+	COMPARE_SCALAR_FIELD(fieldlognum);
 	COMPARE_SCALAR_FIELD(resulttype);
 	COMPARE_SCALAR_FIELD(resulttypmod);
 	COMPARE_SCALAR_FIELD(resultcollid);
diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c
index ce12915592..d80d18cfca 100644
--- a/src/backend/nodes/outfuncs.c
+++ b/src/backend/nodes/outfuncs.c
@@ -1143,6 +1143,7 @@ _outVar(StringInfo str, const Var *node)
 
 	WRITE_INT_FIELD(varno);
 	WRITE_INT_FIELD(varattno);
+	WRITE_INT_FIELD(varnum);
 	WRITE_OID_FIELD(vartype);
 	WRITE_INT_FIELD(vartypmod);
 	WRITE_OID_FIELD(varcollid);
@@ -1421,6 +1422,7 @@ _outFieldSelect(StringInfo str, const FieldSelect *node)
 
 	WRITE_NODE_FIELD(arg);
 	WRITE_INT_FIELD(fieldnum);
+	WRITE_INT_FIELD(fieldlognum);
 	WRITE_OID_FIELD(resulttype);
 	WRITE_INT_FIELD(resulttypmod);
 	WRITE_OID_FIELD(resultcollid);
diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c
index 6a05b69415..f56070bbf6 100644
--- a/src/backend/nodes/readfuncs.c
+++ b/src/backend/nodes/readfuncs.c
@@ -619,6 +619,7 @@ _readVar(void)
 
 	READ_INT_FIELD(varno);
 	READ_INT_FIELD(varattno);
+	READ_INT_FIELD(varnum);
 	READ_OID_FIELD(vartype);
 	READ_INT_FIELD(vartypmod);
 	READ_OID_FIELD(varcollid);
@@ -939,6 +940,7 @@ _readFieldSelect(void)
 
 	READ_NODE_FIELD(arg);
 	READ_INT_FIELD(fieldnum);
+	READ_INT_FIELD(fieldlognum);
 	READ_OID_FIELD(resulttype);
 	READ_INT_FIELD(resulttypmod);
 	READ_OID_FIELD(resultcollid);
diff --git a/src/backend/optimizer/util/plancat.c b/src/backend/optimizer/util/plancat.c
index 5012bfe142..07baf4c9eb 100644
--- a/src/backend/optimizer/util/plancat.c
+++ b/src/backend/optimizer/util/plancat.c
@@ -41,6 +41,7 @@
 #include "optimizer/optimizer.h"
 #include "optimizer/plancat.h"
 #include "optimizer/prep.h"
+#include "optimizer/tlist.h"
 #include "parser/parse_relation.h"
 #include "parser/parsetree.h"
 #include "partitioning/partdesc.h"
@@ -1698,6 +1699,7 @@ build_physical_tlist(PlannerInfo *root, RelOptInfo *rel)
 							  att_tup->atttypmod,
 							  att_tup->attcollation,
 							  0);
+				var->varnum = att_tup->attnum;
 
 				tlist = lappend(tlist,
 								makeTargetEntry((Expr *) var,
diff --git a/src/backend/optimizer/util/tlist.c b/src/backend/optimizer/util/tlist.c
index fe9a9d7d89..82f28c071b 100644
--- a/src/backend/optimizer/util/tlist.c
+++ b/src/backend/optimizer/util/tlist.c
@@ -1056,6 +1056,49 @@ split_pathtarget_at_srfs(PlannerInfo *root,
 	}
 }
 
+/*
+ * Comparator for sorting list of TargetEntries by their logical order.
+ *
+ * The target entry's expr should be either a Var or a FieldSelect.
+ */
+int
+cmp_targetentry_logical_order(const ListCell *a, const ListCell *b)
+{
+	TargetEntry *ta = lfirst_node(TargetEntry, a);
+	TargetEntry *tb = lfirst_node(TargetEntry, b);
+
+	if (IsA(ta->expr, Var))
+	{
+		Var *va;
+		Var *vb;
+
+		Assert(IsA(tb->expr, Var));
+
+		va = (Var *) ta->expr;
+		vb = (Var *) tb->expr;
+
+		if (va->varno != vb->varno)
+			return (va->varno > vb->varno) ? 1 : (va->varno == vb->varno) ? 0 : -1;
+		return (va->varnum > vb->varnum) ? 1 : (va->varnum == vb->varnum) ? 0 : -1;
+	}
+	else
+	{
+		FieldSelect *fa;
+		FieldSelect *fb;
+
+		Assert(IsA(ta->expr, FieldSelect) && IsA(tb->expr, FieldSelect));
+
+		fa = (FieldSelect *) ta->expr;
+		fb = (FieldSelect *) tb->expr;
+
+		Assert(AttributeNumberIsValid(fa->fieldlognum) &&
+			   AttributeNumberIsValid(fb->fieldlognum));
+
+		return (fa->fieldlognum > fb->fieldlognum) ? 1 : (fa->fieldlognum == fb->fieldlognum) ? 0 : -1;
+	}
+
+}
+
 /*
  * Recursively examine expressions for split_pathtarget_at_srfs.
  *
diff --git a/src/backend/parser/analyze.c b/src/backend/parser/analyze.c
index 5ced631957..bf811b3f06 100644
--- a/src/backend/parser/analyze.c
+++ b/src/backend/parser/analyze.c
@@ -1589,7 +1589,7 @@ transformValuesClause(ParseState *pstate, SelectStmt *stmt)
 	 * Generate a targetlist as though expanding "*"
 	 */
 	Assert(pstate->p_next_resno == 1);
-	qry->targetList = expandNSItemAttrs(pstate, nsitem, 0, true, -1);
+	qry->targetList = expandNSItemAttrs(pstate, nsitem, 0, true, false, -1);
 
 	/*
 	 * The grammar allows attaching ORDER BY, LIMIT, and FOR UPDATE to a
diff --git a/src/backend/parser/parse_clause.c b/src/backend/parser/parse_clause.c
index d1b0080268..8aa2fd7d65 100644
--- a/src/backend/parser/parse_clause.c
+++ b/src/backend/parser/parse_clause.c
@@ -1569,6 +1569,7 @@ buildVarFromNSColumn(ParseNamespaceColumn *nscol)
 				  nscol->p_varcollid,
 				  0);
 	/* makeVar doesn't offer parameters for these, so set by hand: */
+	var->varnum = nscol->p_varnum;
 	var->varnosyn = nscol->p_varnosyn;
 	var->varattnosyn = nscol->p_varattnosyn;
 	return var;
diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c
index 27c3abb7ec..8bc9ccbadf 100644
--- a/src/backend/parser/parse_relation.c
+++ b/src/backend/parser/parse_relation.c
@@ -26,6 +26,7 @@
 #include "funcapi.h"
 #include "nodes/makefuncs.h"
 #include "nodes/nodeFuncs.h"
+#include "optimizer/tlist.h"
 #include "parser/parse_enr.h"
 #include "parser/parse_relation.h"
 #include "parser/parse_type.h"
@@ -733,6 +734,7 @@ scanNSItemForColumn(ParseState *pstate, ParseNamespaceItem *nsitem,
 					  nscol->p_varcollid,
 					  sublevels_up);
 		/* makeVar doesn't offer parameters for these, so set them by hand: */
+		var->varnum = nscol->p_varnum;
 		var->varnosyn = nscol->p_varnosyn;
 		var->varattnosyn = nscol->p_varattnosyn;
 	}
@@ -1126,28 +1128,65 @@ buildRelationAliases(TupleDesc tupdesc, Alias *alias, Alias *eref)
 {
 	int			maxattrs = tupdesc->natts;
 	List	   *aliaslist;
-	ListCell   *aliaslc;
 	int			numaliases;
 	int			varattno;
 	int			numdropped = 0;
+	int		   *mappings;
 
 	Assert(eref->colnames == NIL);
 
 	if (alias)
 	{
 		aliaslist = alias->colnames;
-		aliaslc = list_head(aliaslist);
 		numaliases = list_length(aliaslist);
 		/* We'll rebuild the alias colname list */
 		alias->colnames = NIL;
+		mappings = palloc0(sizeof(int) * (maxattrs + 1));
 	}
 	else
 	{
 		aliaslist = NIL;
-		aliaslc = NULL;
 		numaliases = 0;
 	}
 
+	/*
+	 * Build a physical/logical position mapping to emit aliases in logical
+	 * order.
+	 */
+	if (numaliases > 0)
+	{
+		TupleDesc sorted = CreateTupleDescCopy(tupdesc);
+		int i = 1;
+
+		TupleDescSortByAttnum(sorted);
+
+		for (varattno = 0; varattno < maxattrs; varattno++)
+		{
+			Form_pg_attribute attr = TupleDescAttr(sorted, varattno);
+
+			if (attr->attisdropped)
+				continue;
+
+			/*
+			 * We record a 1-based position mapping to be able to use
+			 * AttributeNumberIsValid.
+			 */
+			mappings[attr->attphysnum] = i++;
+
+			/* We're done if we already mapped all the provided aliases. */
+			if (i > numaliases)
+				break;
+		}
+		pfree(sorted);
+
+		/* Too many user-supplied aliases? */
+		if (i <= numaliases)
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_COLUMN_REFERENCE),
+					 errmsg("table \"%s\" has %d columns available but %d columns specified",
+							eref->aliasname, i - 1, numaliases)));
+	}
+
 	for (varattno = 0; varattno < maxattrs; varattno++)
 	{
 		Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno);
@@ -1157,15 +1196,15 @@ buildRelationAliases(TupleDesc tupdesc, Alias *alias, Alias *eref)
 		{
 			/* Always insert an empty string for a dropped column */
 			attrname = makeString(pstrdup(""));
-			if (aliaslc)
-				alias->colnames = lappend(alias->colnames, attrname);
 			numdropped++;
+			if (aliaslist && varattno - numdropped > numaliases)
+				alias->colnames = lappend(alias->colnames, attrname);
 		}
-		else if (aliaslc)
+		else if (aliaslist && AttributeNumberIsValid(mappings[attr->attphysnum]))
 		{
-			/* Use the next user-supplied alias */
-			attrname = lfirst_node(String, aliaslc);
-			aliaslc = lnext(aliaslist, aliaslc);
+			/* Use the next user-supplied alias, in logical order */
+			attrname = list_nth_node(String, aliaslist,
+									 mappings[attr->attphysnum] - 1);
 			alias->colnames = lappend(alias->colnames, attrname);
 		}
 		else
@@ -1176,13 +1215,6 @@ buildRelationAliases(TupleDesc tupdesc, Alias *alias, Alias *eref)
 
 		eref->colnames = lappend(eref->colnames, attrname);
 	}
-
-	/* Too many user-supplied aliases? */
-	if (aliaslc)
-		ereport(ERROR,
-				(errcode(ERRCODE_INVALID_COLUMN_REFERENCE),
-				 errmsg("table \"%s\" has %d columns available but %d columns specified",
-						eref->aliasname, maxattrs - numdropped, numaliases)));
 }
 
 /*
@@ -1262,11 +1294,14 @@ buildNSItemFromTupleDesc(RangeTblEntry *rte, Index rtindex, TupleDesc tupdesc)
 
 		nscolumns[varattno].p_varno = rtindex;
 		nscolumns[varattno].p_varattno = varattno + 1;
+		nscolumns[varattno].p_varattno = attr->attphysnum;
+		nscolumns[varattno].p_varnum = attr->attnum;
 		nscolumns[varattno].p_vartype = attr->atttypid;
 		nscolumns[varattno].p_vartypmod = attr->atttypmod;
 		nscolumns[varattno].p_varcollid = attr->attcollation;
 		nscolumns[varattno].p_varnosyn = rtindex;
 		nscolumns[varattno].p_varattnosyn = varattno + 1;
+		nscolumns[varattno].p_varattnosyn = attr->attphysnum;
 	}
 
 	/* ... and build the nsitem */
@@ -1323,6 +1358,10 @@ buildNSItemFromLists(RangeTblEntry *rte, Index rtindex,
 	{
 		nscolumns[varattno].p_varno = rtindex;
 		nscolumns[varattno].p_varattno = varattno + 1;
+		if (rte->rtekind == RTE_RELATION)
+			nscolumns[varattno].p_varnum = varattno + 1;
+		else
+			nscolumns[varattno].p_varnum = InvalidAttrNumber;
 		nscolumns[varattno].p_vartype = lfirst_oid(lct);
 		nscolumns[varattno].p_vartypmod = lfirst_int(lcm);
 		nscolumns[varattno].p_varcollid = lfirst_oid(lcc);
@@ -1408,6 +1447,9 @@ parserOpenTable(ParseState *pstate, const RangeVar *relation, int lockmode)
  *
  * Note: formerly this checked for refname conflicts, but that's wrong.
  * Caller is responsible for checking for conflicts in the appropriate scope.
+ *
+ * Note also that the ParseNamespaceItem is generated using the range table's
+ * logical order (attnum) rather than physical order (attphysnum).
  */
 ParseNamespaceItem *
 addRangeTableEntry(ParseState *pstate,
@@ -1447,7 +1489,7 @@ addRangeTableEntry(ParseState *pstate,
 
 	/*
 	 * Build the list of effective column names using user-supplied aliases
-	 * and/or actual column names.
+	 * and/or actual column names, using the range table logical order.
 	 */
 	rte->eref = makeAlias(refname, NIL);
 	buildRelationAliases(rel->rd_att, alias, rte->eref);
@@ -3099,6 +3141,7 @@ expandNSItemVars(ParseNamespaceItem *nsitem,
 						  nscol->p_varcollid,
 						  sublevels_up);
 			/* makeVar doesn't offer parameters for these, so set by hand: */
+			var->varnum = nscol->p_varnum;
 			var->varnosyn = nscol->p_varnosyn;
 			var->varattnosyn = nscol->p_varattnosyn;
 			var->location = location;
@@ -3127,13 +3170,15 @@ expandNSItemVars(ParseNamespaceItem *nsitem,
  */
 List *
 expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem,
-				  int sublevels_up, bool require_col_privs, int location)
+				  int sublevels_up, bool require_col_privs, bool logical_order,
+				  int location)
 {
 	RangeTblEntry *rte = nsitem->p_rte;
 	List	   *names,
 			   *vars;
 	ListCell   *name,
-			   *var;
+			   *var,
+			   *lc;
 	List	   *te_list = NIL;
 
 	vars = expandNSItemVars(nsitem, sublevels_up, location, &names);
@@ -3155,8 +3200,12 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem,
 		Var		   *varnode = (Var *) lfirst(var);
 		TargetEntry *te;
 
+		/*
+		 * We don't assign the resno here, as we may have to sort the list by
+		 * logical attnum first.
+		 */
 		te = makeTargetEntry((Expr *) varnode,
-							 (AttrNumber) pstate->p_next_resno++,
+							 0,
 							 label,
 							 false);
 		te_list = lappend(te_list, te);
@@ -3168,6 +3217,17 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem,
 		}
 	}
 
+	if (logical_order)
+		list_sort(te_list, cmp_targetentry_logical_order);
+
+	/* Assign the resnos now that the list is in the final order */
+	foreach(lc, te_list)
+	{
+		TargetEntry *te = (TargetEntry *) lfirst(lc);
+
+		te->resno = (AttrNumber) pstate->p_next_resno++;
+	}
+
 	Assert(name == NULL && var == NULL);	/* lists not the same length? */
 
 	return te_list;
diff --git a/src/backend/parser/parse_target.c b/src/backend/parser/parse_target.c
index cff1cda48d..2b887ee4d9 100644
--- a/src/backend/parser/parse_target.c
+++ b/src/backend/parser/parse_target.c
@@ -20,6 +20,7 @@
 #include "miscadmin.h"
 #include "nodes/makefuncs.h"
 #include "nodes/nodeFuncs.h"
+#include "optimizer/tlist.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_expr.h"
 #include "parser/parse_func.h"
@@ -48,15 +49,18 @@ static Node *transformAssignmentSubscripts(ParseState *pstate,
 										   CoercionContext ccontext,
 										   int location);
 static List *ExpandColumnRefStar(ParseState *pstate, ColumnRef *cref,
-								 bool make_target_entry);
-static List *ExpandAllTables(ParseState *pstate, int location);
+								 bool make_target_entry,
+								 bool logical_order);
+static List *ExpandAllTables(ParseState *pstate, bool logical_order,
+							 int location);
 static List *ExpandIndirectionStar(ParseState *pstate, A_Indirection *ind,
-								   bool make_target_entry, ParseExprKind exprKind);
+								   bool make_target_entry, bool logical_order,
+								   ParseExprKind exprKind);
 static List *ExpandSingleTable(ParseState *pstate, ParseNamespaceItem *nsitem,
 							   int sublevels_up, int location,
-							   bool make_target_entry);
+							   bool make_target_entry, bool logical_order);
 static List *ExpandRowReference(ParseState *pstate, Node *expr,
-								bool make_target_entry);
+								bool make_target_entry, bool logical_order);
 static int	FigureColnameInternal(Node *node, char **name);
 
 
@@ -150,10 +154,12 @@ transformTargetList(ParseState *pstate, List *targetlist,
 
 				if (IsA(llast(cref->fields), A_Star))
 				{
-					/* It is something.*, expand into multiple items */
+					/*
+					 $ It is something.*, expand into multiple items
+					 */
 					p_target = list_concat(p_target,
-										   ExpandColumnRefStar(pstate,
-															   cref,
+										   ExpandColumnRefStar(pstate, cref,
+															   true,
 															   true));
 					continue;
 				}
@@ -169,6 +175,7 @@ transformTargetList(ParseState *pstate, List *targetlist,
 										   ExpandIndirectionStar(pstate,
 																 ind,
 																 true,
+																 true,
 																 exprKind));
 					continue;
 				}
@@ -243,7 +250,7 @@ transformExpressionList(ParseState *pstate, List *exprlist,
 				/* It is something.*, expand into multiple items */
 				result = list_concat(result,
 									 ExpandColumnRefStar(pstate, cref,
-														 false));
+														 false, false));
 				continue;
 			}
 		}
@@ -256,7 +263,8 @@ transformExpressionList(ParseState *pstate, List *exprlist,
 				/* It is something.*, expand into multiple items */
 				result = list_concat(result,
 									 ExpandIndirectionStar(pstate, ind,
-														   false, exprKind));
+														   false, false,
+														   exprKind));
 				continue;
 			}
 		}
@@ -1014,6 +1022,8 @@ checkInsertTargets(ParseState *pstate, List *cols, List **attrnos)
 
 	if (cols == NIL)
 	{
+		TupleDesc tupdesc;
+
 		/*
 		 * Generate default column list for INSERT.
 		 */
@@ -1021,12 +1031,15 @@ checkInsertTargets(ParseState *pstate, List *cols, List **attrnos)
 
 		int			i;
 
+		tupdesc = CreateTupleDescCopy(pstate->p_target_relation->rd_att);
+		TupleDescSortByAttnum(tupdesc);
+
 		for (i = 0; i < numcol; i++)
 		{
 			ResTarget  *col;
 			Form_pg_attribute attr;
 
-			attr = TupleDescAttr(pstate->p_target_relation->rd_att, i);
+			attr = TupleDescAttr(tupdesc, i);
 
 			if (attr->attisdropped)
 				continue;
@@ -1037,8 +1050,10 @@ checkInsertTargets(ParseState *pstate, List *cols, List **attrnos)
 			col->val = NULL;
 			col->location = -1;
 			cols = lappend(cols, col);
-			*attrnos = lappend_int(*attrnos, i + 1);
+			*attrnos = lappend_int(*attrnos, attr->attphysnum);
 		}
+
+		pfree(tupdesc);
 	}
 	else
 	{
@@ -1114,7 +1129,7 @@ checkInsertTargets(ParseState *pstate, List *cols, List **attrnos)
  */
 static List *
 ExpandColumnRefStar(ParseState *pstate, ColumnRef *cref,
-					bool make_target_entry)
+					bool make_target_entry, bool logical_order)
 {
 	List	   *fields = cref->fields;
 	int			numnames = list_length(fields);
@@ -1130,7 +1145,7 @@ ExpandColumnRefStar(ParseState *pstate, ColumnRef *cref,
 		 * need not handle the make_target_entry==false case here.
 		 */
 		Assert(make_target_entry);
-		return ExpandAllTables(pstate, cref->location);
+		return ExpandAllTables(pstate, logical_order, cref->location);
 	}
 	else
 	{
@@ -1169,7 +1184,8 @@ ExpandColumnRefStar(ParseState *pstate, ColumnRef *cref,
 
 			node = pstate->p_pre_columnref_hook(pstate, cref);
 			if (node != NULL)
-				return ExpandRowReference(pstate, node, make_target_entry);
+				return ExpandRowReference(pstate, node, make_target_entry,
+										  logical_order);
 		}
 
 		switch (numnames)
@@ -1234,7 +1250,8 @@ ExpandColumnRefStar(ParseState *pstate, ColumnRef *cref,
 							 errmsg("column reference \"%s\" is ambiguous",
 									NameListToString(cref->fields)),
 							 parser_errposition(pstate, cref->location)));
-				return ExpandRowReference(pstate, node, make_target_entry);
+				return ExpandRowReference(pstate, node, make_target_entry,
+										  logical_order);
 			}
 		}
 
@@ -1270,7 +1287,7 @@ ExpandColumnRefStar(ParseState *pstate, ColumnRef *cref,
 		 * OK, expand the nsitem into fields.
 		 */
 		return ExpandSingleTable(pstate, nsitem, levels_up, cref->location,
-								 make_target_entry);
+								 make_target_entry, logical_order);
 	}
 }
 
@@ -1283,10 +1300,13 @@ ExpandColumnRefStar(ParseState *pstate, ColumnRef *cref,
  * that would include input tables of aliasless JOINs, NEW/OLD pseudo-entries,
  * etc.
  *
+ * If logical_order is true, the expansion is done using the relation logical
+ * order (attnum) rather than physical order.
+ *
  * The referenced relations/columns are marked as requiring SELECT access.
  */
 static List *
-ExpandAllTables(ParseState *pstate, int location)
+ExpandAllTables(ParseState *pstate, bool logical_order, int location)
 {
 	List	   *target = NIL;
 	bool		found_table = false;
@@ -1309,6 +1329,7 @@ ExpandAllTables(ParseState *pstate, int location)
 											   nsitem,
 											   0,
 											   true,
+											   logical_order,
 											   location));
 	}
 
@@ -1339,7 +1360,8 @@ ExpandAllTables(ParseState *pstate, int location)
  */
 static List *
 ExpandIndirectionStar(ParseState *pstate, A_Indirection *ind,
-					  bool make_target_entry, ParseExprKind exprKind)
+					  bool make_target_entry, bool logical_order,
+					  ParseExprKind exprKind)
 {
 	Node	   *expr;
 
@@ -1352,7 +1374,7 @@ ExpandIndirectionStar(ParseState *pstate, A_Indirection *ind,
 	expr = transformExpr(pstate, (Node *) ind, exprKind);
 
 	/* Expand the rowtype expression into individual fields */
-	return ExpandRowReference(pstate, expr, make_target_entry);
+	return ExpandRowReference(pstate, expr, make_target_entry, logical_order);
 }
 
 /*
@@ -1366,12 +1388,14 @@ ExpandIndirectionStar(ParseState *pstate, A_Indirection *ind,
  */
 static List *
 ExpandSingleTable(ParseState *pstate, ParseNamespaceItem *nsitem,
-				  int sublevels_up, int location, bool make_target_entry)
+				  int sublevels_up, int location, bool make_target_entry,
+				  bool logical_order)
 {
 	if (make_target_entry)
 	{
 		/* expandNSItemAttrs handles permissions marking */
-		return expandNSItemAttrs(pstate, nsitem, sublevels_up, true, location);
+		return expandNSItemAttrs(pstate, nsitem, sublevels_up, true,
+								 logical_order, location);
 	}
 	else
 	{
@@ -1413,9 +1437,10 @@ ExpandSingleTable(ParseState *pstate, ParseNamespaceItem *nsitem,
  */
 static List *
 ExpandRowReference(ParseState *pstate, Node *expr,
-				   bool make_target_entry)
+				   bool make_target_entry, bool logical_order)
 {
 	List	   *result = NIL;
+	ListCell   *lc;
 	TupleDesc	tupleDesc;
 	int			numAttrs;
 	int			i;
@@ -1436,7 +1461,9 @@ ExpandRowReference(ParseState *pstate, Node *expr,
 		ParseNamespaceItem *nsitem;
 
 		nsitem = GetNSItemByRangeTablePosn(pstate, var->varno, var->varlevelsup);
-		return ExpandSingleTable(pstate, nsitem, var->varlevelsup, var->location, make_target_entry);
+		return ExpandSingleTable(pstate, nsitem, var->varlevelsup,
+								 var->location, make_target_entry,
+								 logical_order);
 	}
 
 	/*
@@ -1472,6 +1499,7 @@ ExpandRowReference(ParseState *pstate, Node *expr,
 		fselect = makeNode(FieldSelect);
 		fselect->arg = (Expr *) copyObject(expr);
 		fselect->fieldnum = i + 1;
+		fselect->fieldlognum = att->attnum;
 		fselect->resulttype = att->atttypid;
 		fselect->resulttypmod = att->atttypmod;
 		/* save attribute's collation for parse_collate.c */
@@ -1482,8 +1510,12 @@ ExpandRowReference(ParseState *pstate, Node *expr,
 			/* add TargetEntry decoration */
 			TargetEntry *te;
 
+			/*
+			 * We don't assign the resno here, as we may have to sort the list
+			 * by logical attnum first.
+			 */
 			te = makeTargetEntry((Expr *) fselect,
-								 (AttrNumber) pstate->p_next_resno++,
+								 0,
 								 pstrdup(NameStr(att->attname)),
 								 false);
 			result = lappend(result, te);
@@ -1492,6 +1524,24 @@ ExpandRowReference(ParseState *pstate, Node *expr,
 			result = lappend(result, fselect);
 	}
 
+	/*
+	 * If caller didn't ask to make target entries, we can return the list now
+	 */
+	if (!make_target_entry)
+		return result;
+
+	/* If caller asked for logical order, sort the target entries */
+	if (logical_order)
+		list_sort(result, cmp_targetentry_logical_order);
+
+	/* And assign the resnos now that the list is in the final order */
+	foreach(lc, result)
+	{
+		TargetEntry *te = (TargetEntry *) lfirst(lc);
+
+		te->resno = (AttrNumber) pstate->p_next_resno++;
+	}
+
 	return result;
 }
 
diff --git a/src/backend/parser/parse_utilcmd.c b/src/backend/parser/parse_utilcmd.c
index 69132f729d..33e571b3b4 100644
--- a/src/backend/parser/parse_utilcmd.c
+++ b/src/backend/parser/parse_utilcmd.c
@@ -1001,7 +1001,13 @@ transformTableLikeClause(CreateStmtContext *cxt, TableLikeClause *table_like_cla
 						   RelationGetRelationName(relation));
 	}
 
-	tupleDesc = RelationGetDescr(relation);
+	/*
+	 * The expansion has to be done in the logical order (attnum) rather than
+	 * the physical order, and the code also needs to access the constraints so
+	 * make a full copy of the tuple desc.
+	 */
+	tupleDesc = CreateTupleDescCopyConstr(RelationGetDescr(relation));
+	TupleDescSortByAttnum(tupleDesc);
 
 	/*
 	 * Insert the copied attributes into the cxt for the new table definition.
@@ -1110,6 +1116,7 @@ transformTableLikeClause(CreateStmtContext *cxt, TableLikeClause *table_like_cla
 			cxt->alist = lappend(cxt->alist, stmt);
 		}
 	}
+	pfree(tupleDesc);
 
 	/*
 	 * We cannot yet deal with defaults, CHECK constraints, or indexes, since
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index 8a98060956..febc305244 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -576,8 +576,11 @@ RelationBuildTupleDesc(Relation relation)
 
 		attphysnum = attp->attphysnum;
 		if (attphysnum <= 0 || attphysnum > RelationGetNumberOfAttributes(relation))
-			elog(ERROR, "invalid attribute number %d for relation \"%s\"",
+			elog(ERROR, "invalid physical attribute number %d for relation \"%s\"",
 				 attp->attphysnum, RelationGetRelationName(relation));
+		if (attp->attnum <= 0 || attp->attnum > RelationGetNumberOfAttributes(relation))
+			elog(ERROR, "invalid logical attribute number  %d for relation \"%s\"",
+				 attp->attnum, RelationGetRelationName(relation));
 
 		memcpy(TupleDescAttr(relation->rd_att, attphysnum - 1),
 			   attp,
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index a25ec298fd..0e56b546e3 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -1534,6 +1534,7 @@ describeOneTableDetails(const char *schemaname,
 		char		relpersistence;
 		char		relreplident;
 		char	   *relam;
+		bool		haslogicalorder;
 	}			tableinfo;
 	bool		show_column_details = false;
 
@@ -1546,7 +1547,28 @@ describeOneTableDetails(const char *schemaname,
 	initPQExpBuffer(&tmpbuf);
 
 	/* Get general table info */
-	if (pset.sversion >= 120000)
+	if (pset.sversion >= 150000) /* FXME - bump me when pg15 branched */
+	{
+		printfPQExpBuffer(&buf,
+						  "SELECT c.relchecks, c.relkind, c.relhasindex, c.relhasrules, "
+						  "c.relhastriggers, c.relrowsecurity, c.relforcerowsecurity, "
+						  "false AS relhasoids, c.relispartition, %s, c.reltablespace, "
+						  "CASE WHEN c.reloftype = 0 THEN '' ELSE c.reloftype::pg_catalog.regtype::pg_catalog.text END, "
+						  "c.relpersistence, c.relreplident, am.amname,\n"
+						  "  (SELECT count(*) > 0 FROM pg_catalog.pg_attribute a\n"
+						  "   WHERE a.attrelid = c.oid\n"
+						  "   AND a.attphysnum != a.attnum)\n"
+						  "FROM pg_catalog.pg_class c\n "
+						  "LEFT JOIN pg_catalog.pg_class tc ON (c.reltoastrelid = tc.oid)\n"
+						  "LEFT JOIN pg_catalog.pg_am am ON (c.relam = am.oid)\n"
+						  "WHERE c.oid = '%s';",
+						  (verbose ?
+						   "pg_catalog.array_to_string(c.reloptions || "
+						   "array(select 'toast.' || x from pg_catalog.unnest(tc.reloptions) x), ', ')\n"
+						   : "''"),
+						  oid);
+	}
+	else if (pset.sversion >= 120000)
 	{
 		printfPQExpBuffer(&buf,
 						  "SELECT c.relchecks, c.relkind, c.relhasindex, c.relhasrules, "
@@ -1666,6 +1688,11 @@ describeOneTableDetails(const char *schemaname,
 			(char *) NULL : pg_strdup(PQgetvalue(res, 0, 14));
 	else
 		tableinfo.relam = NULL;
+	/* FIXME - bump me when pg15 branched */
+	if (pset.sversion >= 150000)
+		tableinfo.haslogicalorder = strcmp(PQgetvalue(res, 0, 15), "t") == 0;
+	else
+		tableinfo.haslogicalorder = false;
 	PQclear(res);
 	res = NULL;
 
@@ -1955,16 +1982,8 @@ describeOneTableDetails(const char *schemaname,
 	}
 
 	appendPQExpBufferStr(&buf, "\nFROM pg_catalog.pg_attribute a");
-	if (pset.sversion >= 150000) /* FIXME - bump me when pg15 branched */
-	{
-		appendPQExpBuffer(&buf, "\nWHERE a.attrelid = '%s' AND a.attphysnum > 0 AND NOT a.attisdropped", oid);
-		appendPQExpBufferStr(&buf, "\nORDER BY a.attphysnum;");
-	}
-	else
-	{
-		appendPQExpBuffer(&buf, "\nWHERE a.attrelid = '%s' AND a.attnum > 0 AND NOT a.attisdropped", oid);
-		appendPQExpBufferStr(&buf, "\nORDER BY a.attnum;");
-	}
+	appendPQExpBuffer(&buf, "\nWHERE a.attrelid = '%s' AND a.attnum > 0 AND NOT a.attisdropped", oid);
+	appendPQExpBufferStr(&buf, "\nORDER BY a.attnum;");
 
 	res = PSQLexec(buf.data);
 	if (!res)
@@ -2386,6 +2405,36 @@ describeOneTableDetails(const char *schemaname,
 		PGresult   *result = NULL;
 		int			tuples = 0;
 
+		/*
+		 * print physical attribute order if different from logical
+		 * FIXME - bump me when pg15 branched
+		 */
+		if (pset.sversion >= 150000 && tableinfo.haslogicalorder && verbose)
+		{
+			Assert(tableinfo.relkind == RELKIND_RELATION);
+
+			printfPQExpBuffer(&buf,
+					"SELECT string_agg(a.attname, ', ')\n"
+					"FROM pg_catalog.pg_class c\n"
+					"JOIN pg_catalog.pg_attribute a ON a.attrelid = c.oid\n"
+					"WHERE c.oid = '%s' \n"
+					"AND a.attphysnum > 0 \n"
+					"AND NOT a.attisdropped", oid);
+			result = PSQLexec(buf.data);
+			if (!result)
+				goto error_return;
+			else
+				tuples = PQntuples(result);
+
+			Assert(tuples == 1);
+
+			printfPQExpBuffer(&buf, "(%s)", PQgetvalue(result, 0, 0));
+			PQclear(result);
+
+			printTableAddFooter(&cont, _("Physical order:"));
+			printTableAddFooter(&cont, buf.data);
+		}
+
 		/* print indexes */
 		if (tableinfo.hasindex)
 		{
diff --git a/src/include/access/tupdesc.h b/src/include/access/tupdesc.h
index 28dd6de18b..f519758b22 100644
--- a/src/include/access/tupdesc.h
+++ b/src/include/access/tupdesc.h
@@ -147,6 +147,8 @@ extern void TupleDescInitEntryCollation(TupleDesc desc,
 										AttrNumber attributeNumber,
 										Oid collationid);
 
+extern void TupleDescSortByAttnum(TupleDesc desc);
+
 extern TupleDesc BuildDescForRelation(List *schema);
 
 extern TupleDesc BuildDescFromLists(List *names, List *types, List *typmods, List *collations);
diff --git a/src/include/catalog/pg_attribute.h b/src/include/catalog/pg_attribute.h
index 746566eb8d..d2e8b5f765 100644
--- a/src/include/catalog/pg_attribute.h
+++ b/src/include/catalog/pg_attribute.h
@@ -82,6 +82,11 @@ CATALOG(pg_attribute,1249,AttributeRelationId) BKI_BOOTSTRAP BKI_ROWTYPE_OID(75,
 	 */
 	int16		attphysnum;
 
+	/* FIXME Same as attphysnum, but for the "logical" order, e.g. for a
+	 * star-expansion.
+	 */
+	int16		attnum;
+
 	/*
 	 * attndims is the declared number of dimensions, if an array type,
 	 * otherwise zero.
@@ -207,6 +212,7 @@ CATALOG(pg_attribute,1249,AttributeRelationId) BKI_BOOTSTRAP BKI_ROWTYPE_OID(75,
 typedef FormData_pg_attribute *Form_pg_attribute;
 
 DECLARE_UNIQUE_INDEX(pg_attribute_relid_attnam_index, 2658, AttributeRelidNameIndexId, on pg_attribute using btree(attrelid oid_ops, attname name_ops));
+DECLARE_UNIQUE_INDEX(pg_attribute_relid_attnum_index, 8294, AttributeReliNumIndexId, on pg_attribute using btree(attrelid oid_ops, attnum int2_ops));
 DECLARE_UNIQUE_INDEX_PKEY(pg_attribute_relid_attphysnum_index, 2659, AttributeRelidPhysNumIndexId, on pg_attribute using btree(attrelid oid_ops, attphysnum int2_ops));
 
 #ifdef EXPOSE_TO_CLIENT_CODE
diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h
index 51505eee85..7dad6f4fb6 100644
--- a/src/include/nodes/primnodes.h
+++ b/src/include/nodes/primnodes.h
@@ -199,6 +199,7 @@ typedef struct Var
 								 * table, or INNER_VAR/OUTER_VAR/etc */
 	AttrNumber	varattno;		/* attribute number of this var, or zero for
 								 * all attrs ("whole-row Var") */
+	AttrNumber	varnum;			/* logical attribute number of this var FIXME */
 	Oid			vartype;		/* pg_type OID for the type of this var */
 	int32		vartypmod;		/* pg_attribute typmod value */
 	Oid			varcollid;		/* OID of collation, or InvalidOid if none */
@@ -815,7 +816,8 @@ typedef struct FieldSelect
 {
 	Expr		xpr;
 	Expr	   *arg;			/* input expression */
-	AttrNumber	fieldnum;		/* attribute number of field to extract */
+	AttrNumber	fieldnum;		/* physical attribute number of field to extract */
+	AttrNumber	fieldlognum;	/* logical attribute number of field (can be 0) */
 	Oid			resulttype;		/* type of the field (result type of this
 								 * node) */
 	int32		resulttypmod;	/* output typmod (usually -1) */
diff --git a/src/include/optimizer/tlist.h b/src/include/optimizer/tlist.h
index 04668ba1c0..5e4fb21bc8 100644
--- a/src/include/optimizer/tlist.h
+++ b/src/include/optimizer/tlist.h
@@ -48,6 +48,7 @@ extern void apply_pathtarget_labeling_to_tlist(List *tlist, PathTarget *target);
 extern void split_pathtarget_at_srfs(PlannerInfo *root,
 									 PathTarget *target, PathTarget *input_target,
 									 List **targets, List **targets_contain_srfs);
+extern int cmp_targetentry_logical_order(const ListCell *a, const ListCell *b);
 
 /* Convenience macro to get a PathTarget with valid cost/width fields */
 #define create_pathtarget(root, tlist) \
diff --git a/src/include/parser/parse_node.h b/src/include/parser/parse_node.h
index cf9c759025..642ef43b32 100644
--- a/src/include/parser/parse_node.h
+++ b/src/include/parser/parse_node.h
@@ -300,7 +300,8 @@ struct ParseNamespaceItem
 struct ParseNamespaceColumn
 {
 	Index		p_varno;		/* rangetable index */
-	AttrNumber	p_varattno;		/* attribute number of the column */
+	AttrNumber	p_varattno;		/* physical attribute number of the column */
+	AttrNumber	p_varnum;		/* logical attribute number of the column */
 	Oid			p_vartype;		/* pg_type OID */
 	int32		p_vartypmod;	/* type modifier value */
 	Oid			p_varcollid;	/* OID of collation, or InvalidOid */
diff --git a/src/include/parser/parse_relation.h b/src/include/parser/parse_relation.h
index de21c3c649..97e1c3f453 100644
--- a/src/include/parser/parse_relation.h
+++ b/src/include/parser/parse_relation.h
@@ -114,7 +114,7 @@ extern List *expandNSItemVars(ParseNamespaceItem *nsitem,
 							  List **colnames);
 extern List *expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem,
 							   int sublevels_up, bool require_col_privs,
-							   int location);
+							   bool logical_order, int location);
 extern int	attnameAttNum(Relation rd, const char *attname, bool sysColOK);
 extern const NameData *attnumAttName(Relation rd, int attid);
 extern Oid	attnumTypeId(Relation rd, int attid);
diff --git a/src/test/regress/regress.c b/src/test/regress/regress.c
index f34c8474b4..fbd079f8b2 100644
--- a/src/test/regress/regress.c
+++ b/src/test/regress/regress.c
@@ -414,9 +414,22 @@ ttdummy(PG_FUNCTION_ARGS)
 		query = (char *) palloc(100 + 16 * natts);
 
 		/*
-		 * Construct query: INSERT INTO _relation_ VALUES ($1, ...)
+		 * Construct query:
+		 * INSERT INTO _relation_ (field1, ...) VALUES ($1, ...)
+		 * We have to specify the fields explicitly as we provide the value in
+		 * the relation physical order and not the possibly different logical
+		 * order.
 		 */
-		sprintf(query, "INSERT INTO %s VALUES (", relname);
+		sprintf(query, "INSERT INTO %s(", relname);
+		for (i = 0; i < natts; i++)
+		{
+			if (i > 0)
+				sprintf(query + strlen(query), ", ");
+
+			sprintf(query + strlen(query),
+					"%s", NameStr(TupleDescAttr(tupdesc, i)->attname));
+		}
+		sprintf(query + strlen(query), ") VALUES (");
 		for (i = 1; i <= natts; i++)
 		{
 			sprintf(query + strlen(query), "$%d%s",
-- 
2.33.1

