diff --git a/src/backend/nodes/gen_node_support.pl b/src/backend/nodes/gen_node_support.pl
index 6aaf401a72..c77a130054 100644
--- a/src/backend/nodes/gen_node_support.pl
+++ b/src/backend/nodes/gen_node_support.pl
@@ -40,8 +40,10 @@ my @node_types = qw(Node);
 # collect info for each node type
 my %node_type_info;
 
-# node types we don't want copy/equal support for
-my @no_copy_equal;
+# node types we don't want copy support for
+my @no_copy;
+# node types we don't want equal support for
+my @no_equal;
 # node types we don't want read support for
 my @no_read;
 # node types we don't want read/write support for
@@ -90,8 +92,21 @@ push @scalar_types, qw(QualCost);
 
 # XXX various things we are not publishing right now to stay level
 # with the manual system
-push @no_copy_equal, qw(CallContext InlineCodeBlock);
+push @no_copy, qw(CallContext InlineCodeBlock);
+push @no_equal, qw(CallContext InlineCodeBlock);
 push @no_read_write, qw(AccessPriv AlterTableCmd CallContext CreateOpClassItem FunctionParameter InferClause InlineCodeBlock ObjectWithArgs OnConflictClause PartitionCmd RoleSpec VacuumRelation);
+push @no_read, qw(A_ArrayExpr A_Indices A_Indirection AlterStatsStmt
+CollateClause ColumnDef ColumnRef CreateForeignTableStmt CreateStatsStmt
+CreateStmt FuncCall ImportForeignSchemaStmt IndexElem IndexStmt
+JsonAggConstructor JsonArgument JsonArrayAgg JsonArrayConstructor
+JsonArrayQueryConstructor JsonCommon JsonFuncExpr JsonKeyValue
+JsonObjectAgg JsonObjectConstructor JsonOutput JsonParseExpr JsonScalarExpr
+JsonSerializeExpr JsonTable JsonTableColumn JsonTablePlan LockingClause
+MultiAssignRef PLAssignStmt ParamRef PartitionElem PartitionSpec
+PlaceHolderVar PublicationObjSpec PublicationTable RangeFunction
+RangeSubselect RangeTableFunc RangeTableFuncCol RangeTableSample RawStmt
+ResTarget ReturnStmt SelectStmt SortBy StatsElem TableLikeClause
+TriggerTransition TypeCast TypeName WindowDef WithClause XmlSerialize);
 
 
 ## read input
@@ -201,14 +216,16 @@ foreach my $infile (@ARGV)
 						qw(execnodes.h trigger.h event_trigger.h amapi.h tableam.h
 							tsmapi.h fdwapi.h tuptable.h replnodes.h supportnodes.h))
 					{
-						push @no_copy_equal, $in_struct;
+						push @no_copy, $in_struct;
+						push @no_equal, $in_struct;
 						push @no_read_write, $in_struct;
 					}
 
 					# Propagate some node attributes from supertypes
 					if ($supertype)
 					{
-						push @no_copy_equal, $in_struct if elem $supertype, @no_copy_equal;
+						push @no_copy, $in_struct if elem $supertype, @no_copy;
+						push @no_equal, $in_struct if elem $supertype, @no_equal;
 						push @no_read, $in_struct if elem $supertype, @no_read;
 					}
 				}
@@ -245,7 +262,9 @@ foreach my $infile (@ARGV)
 						foreach my $attr (@attrs)
 						{
 							if ($attr !~ /^array_size\(\w+\)$/ &&
-								!elem $attr, qw(copy_ignore equal_ignore equal_ignore_if_zero read_write_ignore
+								$attr !~ /^copy_as\(\w+\)$/ &&
+								$attr !~ /^read_as\(\w+\)$/ &&
+								!elem $attr, qw(equal_ignore equal_ignore_if_zero read_write_ignore
 									write_only_relids write_only_nondefault_pathtarget write_only_req_outer))
 							{
 								die "$infile:$.: unrecognized attribute \"$attr\"\n";
@@ -291,9 +310,18 @@ foreach my $infile (@ARGV)
 					{
 						push @custom_read_write, $in_struct;
 					}
+					elsif ($attr eq 'no_copy')
+					{
+						push @no_copy, $in_struct;
+					}
+					elsif ($attr eq 'no_equal')
+					{
+						push @no_equal, $in_struct;
+					}
 					elsif ($attr eq 'no_copy_equal')
 					{
-						push @no_copy_equal, $in_struct;
+						push @no_copy, $in_struct;
+						push @no_equal, $in_struct;
 					}
 					elsif ($attr eq 'no_read')
 					{
@@ -391,16 +419,18 @@ print $eff $node_includes;
 foreach my $n (@node_types)
 {
 	next if elem $n, @abstract_types;
-	next if elem $n, @no_copy_equal;
+	my $struct_no_copy = (elem $n, @no_copy);
+	my $struct_no_equal = (elem $n, @no_equal);
+	next if $struct_no_copy && $struct_no_equal;
 	next if $n eq 'List';
 
 	print $cfs "\t\tcase T_${n}:\n".
 	  "\t\t\tretval = _copy${n}(from);\n".
-	  "\t\t\tbreak;\n";
+	  "\t\t\tbreak;\n" unless $struct_no_copy;
 
 	print $efs "\t\tcase T_${n}:\n".
 	  "\t\t\tretval = _equal${n}(a, b);\n".
-	  "\t\t\tbreak;\n";
+	  "\t\t\tbreak;\n" unless $struct_no_equal;
 
 	next if elem $n, @custom_copy_equal;
 
@@ -410,21 +440,47 @@ _copy${n}(const $n *from)
 {
 \t${n} *newnode = makeNode($n);
 
-";
+" unless $struct_no_copy;
 
 	print $eff "
 static bool
 _equal${n}(const $n *a, const $n *b)
 {
-";
+" unless $struct_no_equal;
 
 	# print instructions for each field
 	foreach my $f (@{$node_type_info{$n}->{fields}})
 	{
 		my $t = $node_type_info{$n}->{field_types}{$f};
 		my @a = @{ $node_type_info{$n}->{field_attrs}{$f} };
-		my $copy_ignore = (elem 'copy_ignore', @a);
-		my $equal_ignore = (elem 'equal_ignore', @a);
+		my $copy_ignore = $struct_no_copy;
+		my $equal_ignore = $struct_no_equal;
+
+		# extract per-field attributes
+		my $array_size_field;
+		my $copy_as_field;
+		foreach my $a (@a)
+		{
+			if ($a =~ /^array_size.([\w.]+)/)
+			{
+				$array_size_field = $1;
+			}
+			elsif ($a =~ /^copy_as.([\w.]+)/)
+			{
+				$copy_as_field = $1;
+			}
+			elsif ($a eq 'equal_ignore')
+			{
+				$equal_ignore = 1;
+			}
+		}
+
+		# override type-specific copy method if copy_as is specified
+		if ($copy_as_field)
+		{
+			print $cff "\tnewnode->$f = $copy_as_field;\n" unless $copy_ignore;
+			$copy_ignore = 1;
+		}
 
 		# select instructions by field type
 		if ($t eq 'char*')
@@ -458,15 +514,6 @@ _equal${n}(const $n *a, const $n *b)
 		elsif ($t =~ /(\w+)\*/ and elem $1, @scalar_types)
 		{
 			my $tt = $1;
-			my $array_size_field;
-			foreach my $a (@a)
-			{
-				if ($a =~ /^array_size.([\w.]+)/)
-				{
-					$array_size_field = $1;
-					last;
-				}
-			}
 			if (!$array_size_field)
 			{
 				die "no array size defined for $n.$f of type $t";
@@ -511,11 +558,11 @@ _equal${n}(const $n *a, const $n *b)
 	print $cff "
 \treturn newnode;
 }
-";
+" unless $struct_no_copy;
 	print $eff "
 \treturn true;
 }
-";
+" unless $struct_no_equal;
 }
 
 close $cff;
@@ -546,18 +593,17 @@ foreach my $n (@node_types)
 		next unless elem $n, @keep;
 	}
 
-	my $no_read = (elem $n, @no_read);
+	my $struct_no_read = (elem $n, @no_read);
 
-	# output format starts with upper case node type, underscores stripped
+	# output format starts with upper case node type name
 	my $N = uc $n;
-	$N =~ s/_//g;
 
 	print $ofs "\t\t\tcase T_${n}:\n".
 	  "\t\t\t\t_out${n}(str, obj);\n".
 	  "\t\t\t\tbreak;\n";
 
 	print $rfs "\telse if (MATCH(\"$N\", " . length($N) . "))\n".
-	  "\t\treturn_value = _read${n}();\n" unless $no_read;
+	  "\t\treturn_value = _read${n}();\n" unless $struct_no_read;
 
 	next if elem $n, @custom_read_write;
 
@@ -575,18 +621,47 @@ _read${n}(void)
 {
 \tREAD_LOCALS($n);
 
-" unless $no_read;
+" unless $struct_no_read;
 
 	# print instructions for each field
 	foreach my $f (@{$node_type_info{$n}->{fields}})
 	{
 		my $t = $node_type_info{$n}->{field_types}{$f};
 		my @a = @{ $node_type_info{$n}->{field_attrs}{$f} };
-		next if (elem 'read_write_ignore', @a);
+		my $no_read = $struct_no_read;
+
+		# extract per-field attributes
+		my $read_write_ignore = 0;
+		my $read_as_field;
+		foreach my $a (@a)
+		{
+			if ($a =~ /^read_as.([\w.]+)/)
+			{
+				$read_as_field = $1;
+			}
+			elsif ($a eq 'read_write_ignore')
+			{
+				$read_write_ignore = 1;
+			}
+		}
 
 		# XXX Previously, for subtyping, only the leaf field name is
 		# used. Ponder whether we want to keep it that way.
 
+		# override type-specific read method if read_as is specified
+		if ($read_as_field)
+		{
+			print $rff "\tlocal_node->$f = $read_as_field;\n" unless $no_read;
+			$no_read = 1;
+		}
+
+		# check this after handling read_as
+		if ($read_write_ignore)
+		{
+			next if $no_read;
+			die "$n.$f must not be marked read_write_ignore\n";
+		}
+
 		# select instructions by field type
 		if ($t eq 'bool')
 		{
@@ -712,7 +787,8 @@ _read${n}(void)
 		}
 		elsif ($t eq 'ParamPathInfo*' && elem 'write_only_req_outer', @a)
 		{
-			print $off "\tif (node->$f)\n".
+			print $off "\tappendStringInfoString(str, \" :required_outer \");\n".
+			  "\tif (node->$f)\n".
 			  "\t\toutBitmapset(str, node->$f->ppi_req_outer);\n".
 			  "\telse\n".
 			  "\t\toutBitmapset(str, NULL);\n";
@@ -754,7 +830,7 @@ _read${n}(void)
 	print $rff "
 \tREAD_DONE();
 }
-" unless $no_read;
+" unless $struct_no_read;
 }
 
 close $off;
diff --git a/src/include/nodes/nodes.h b/src/include/nodes/nodes.h
index d77aad6473..9166903606 100644
--- a/src/include/nodes/nodes.h
+++ b/src/include/nodes/nodes.h
@@ -590,26 +590,39 @@ typedef enum NodeTag
  * - custom_read_write: Has custom implementations in outfuncs.c and
  *   readfuncs.c.
  *
- * - no_copy_equal: Does not support copyObject() and equal() at all.
+ * - no_copy: Does not support copyObject() at all.
+ *
+ * - no_equal: Does not support equal() at all.
+ *
+ * - no_copy_equal: Shorthand for both no_copy and no_equal.
  *
  * - no_read: Does not support nodeRead() at all.
  *
  * - special_read_write: Has special treatment in outNode() and nodeRead().
  *
+ * Node types can be supertypes of other types whether or not they are marked
+ * abstract: if a node struct appears as the first field of another struct
+ * type, then it is the supertype of that type.  The no_copy, no_equal,
+ * no_copy_equal, and no_read node attributes are automatically inherited
+ * from the supertype.
+ *
  * Valid node field attributes:
  *
  * - array_size(OTHERFIELD): This field is a dynamically allocated array with
  *   size indicated by the mentioned other field.  The other field is either a
  *   scalar or a list, in which case the length of the list is used.
  *
- * - copy_ignore: Ignore the field for copy.
+ * - copy_as(VALUE): In copyObject(), replace the field's value with VALUE.
  *
  * - equal_ignore: Ignore the field for equality.
  *
  * - equal_ignore_if_zero: Ignore the field for equality if it is zero.
  *   (Otherwise, compare normally.)
  *
- * - read_write_ignore: Ignore the field for read/write.
+ * - read_as(VALUE): In nodeRead(), replace the field's value with VALUE.
+ *
+ * - read_write_ignore: Ignore the field for read/write.  This is only allowed
+ *   if the node type is marked no_read or read_as() is also specified.
  *
  * - write_only_relids, write_only_nondefault_pathtarget, write_only_req_outer:
  *   Special handling for Path struct; see there.
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index fb026c6b1f..d870d3b09c 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -127,7 +127,7 @@ typedef struct Query pg_node_attr(custom_read_write)
 	 * query identifier (can be set by plugins); ignored for equal, might not
 	 * be set
 	 */
-	uint64		queryId pg_node_attr(equal_ignore);
+	uint64		queryId pg_node_attr(equal_ignore, read_as(0));
 
 	bool		canSetTag;		/* do I set the command result tag? */
 
diff --git a/src/include/nodes/pathnodes.h b/src/include/nodes/pathnodes.h
index 4212610d5e..77fb3c0b8a 100644
--- a/src/include/nodes/pathnodes.h
+++ b/src/include/nodes/pathnodes.h
@@ -92,7 +92,7 @@ typedef enum UpperRelationKind
  * the field type.)
  *----------
  */
-typedef struct PlannerGlobal pg_node_attr(no_copy_equal)
+typedef struct PlannerGlobal pg_node_attr(no_copy_equal, no_read)
 {
 	NodeTag		type;
 
@@ -163,7 +163,7 @@ typedef struct PlannerInfo PlannerInfo;
 #define HAVE_PLANNERINFO_TYPEDEF 1
 #endif
 
-struct PlannerInfo pg_node_attr(no_copy_equal)
+struct PlannerInfo pg_node_attr(no_copy_equal, no_read)
 {
 	NodeTag		type;
 
@@ -376,7 +376,7 @@ struct PlannerInfo pg_node_attr(no_copy_equal)
 
 	/* These fields are used only when hasRecursion is true: */
 	int			wt_param_id;	/* PARAM_EXEC ID for the work table */
-	struct Path *non_recursive_path;	/* a path for non-recursive term */
+	struct Path *non_recursive_path pg_node_attr(read_write_ignore);	/* a path for non-recursive term */
 
 	/* These fields are workspace for createplan.c */
 	Relids		curOuterRels;	/* outer rels above current node */
@@ -691,7 +691,7 @@ typedef enum RelOptKind
 	 (rel)->reloptkind == RELOPT_OTHER_JOINREL || \
 	 (rel)->reloptkind == RELOPT_OTHER_UPPER_REL)
 
-typedef struct RelOptInfo pg_node_attr(no_copy_equal)
+typedef struct RelOptInfo pg_node_attr(no_copy_equal, no_read)
 {
 	NodeTag		type;
 
@@ -922,7 +922,7 @@ typedef struct IndexOptInfo IndexOptInfo;
 #define HAVE_INDEXOPTINFO_TYPEDEF 1
 #endif
 
-struct IndexOptInfo pg_node_attr(no_copy_equal)
+struct IndexOptInfo pg_node_attr(no_copy_equal, no_read)
 {
 	NodeTag		type;
 
@@ -1081,7 +1081,7 @@ typedef struct ForeignKeyOptInfo pg_node_attr(custom_read_write, no_copy_equal,
  * Each pg_statistic_ext row is represented by one or more nodes of this
  * type, or even zero if ANALYZE has not computed them.
  */
-typedef struct StatisticExtInfo pg_node_attr(no_copy_equal)
+typedef struct StatisticExtInfo pg_node_attr(no_copy_equal, no_read)
 {
 	NodeTag		type;
 
@@ -1146,6 +1146,10 @@ typedef struct StatisticExtInfo pg_node_attr(no_copy_equal)
  *
  * NB: if ec_merged isn't NULL, this class has been merged into another, and
  * should be ignored in favor of using the pointed-to class.
+ *
+ * NB: EquivalenceClasses are never copied after creation.  Therefore,
+ * copyObject() copies pointers to them as pointers, and equal() compares
+ * pointers to EquivalenceClasses via pointer equality.
  */
 typedef struct EquivalenceClass pg_node_attr(custom_read_write, no_copy_equal, no_read)
 {
@@ -1197,7 +1201,7 @@ typedef struct EquivalenceClass pg_node_attr(custom_read_write, no_copy_equal, n
  * anyarray_ops would never work without this.  Use em_datatype when
  * looking up a specific btree operator to work with this expression.
  */
-typedef struct EquivalenceMember pg_node_attr(no_copy_equal)
+typedef struct EquivalenceMember pg_node_attr(no_copy_equal, no_read)
 {
 	NodeTag		type;
 
@@ -1226,7 +1230,7 @@ typedef struct EquivalenceMember pg_node_attr(no_copy_equal)
  * BTGreaterStrategyNumber (for DESC).  We assume that all ordering-capable
  * index types will use btree-compatible strategy numbers.
  */
-typedef struct PathKey
+typedef struct PathKey pg_node_attr(no_read)
 {
 	NodeTag		type;
 
@@ -1239,7 +1243,7 @@ typedef struct PathKey
 /*
  * Combines information about pathkeys and the associated clauses.
  */
-typedef struct PathKeyInfo
+typedef struct PathKeyInfo pg_node_attr(no_read)
 {
 	NodeTag		type;
 	List	   *pathkeys;
@@ -1320,7 +1324,7 @@ typedef struct PathTarget pg_node_attr(no_copy_equal, no_read)
  * on how the join is formed.  The relevant clauses will appear in each
  * parameterized join path's joinrestrictinfo list, instead.
  */
-typedef struct ParamPathInfo pg_node_attr(no_copy_equal)
+typedef struct ParamPathInfo pg_node_attr(no_copy_equal, no_read)
 {
 	NodeTag		type;
 
@@ -1498,7 +1502,7 @@ typedef struct IndexPath
  * column, i.e. the indexcol values must form a nondecreasing sequence.
  * (The order of multiple clauses for the same index column is unspecified.)
  */
-typedef struct IndexClause pg_node_attr(no_copy_equal)
+typedef struct IndexClause pg_node_attr(no_copy_equal, no_read)
 {
 	NodeTag		type;
 	struct RestrictInfo *rinfo; /* original restriction or join clause */
@@ -1995,14 +1999,14 @@ typedef struct AggPath
  * Various annotations used for grouping sets in the planner.
  */
 
-typedef struct GroupingSetData pg_node_attr(no_copy_equal)
+typedef struct GroupingSetData pg_node_attr(no_copy_equal, no_read)
 {
 	NodeTag		type;
 	List	   *set;			/* grouping set as list of sortgrouprefs */
 	Cardinality numGroups;		/* est. number of result groups */
 } GroupingSetData;
 
-typedef struct RollupData pg_node_attr(no_copy_equal)
+typedef struct RollupData pg_node_attr(no_copy_equal, no_read)
 {
 	NodeTag		type;
 	List	   *groupClause;	/* applicable subset of parse->groupClause */
@@ -2275,7 +2279,7 @@ typedef struct LimitPath
  * recursion in plan tree dump.
  */
 
-typedef struct RestrictInfo
+typedef struct RestrictInfo pg_node_attr(no_read)
 {
 	NodeTag		type;
 
@@ -2370,10 +2374,10 @@ typedef struct RestrictInfo
 
 	/*
 	 * List of MergeScanSelCache structs.  Those aren't Nodes, so hard to
-	 * copy.  Ignoring it will have the effect that copying will just reset
-	 * the cache.
+	 * copy; instead replace with NIL.  That has the effect that copying will
+	 * just reset the cache.  Likewise, can't compare or print them.
 	 */
-	List	   *scansel_cache pg_node_attr(copy_ignore, equal_ignore);
+	List	   *scansel_cache pg_node_attr(copy_as(NIL), equal_ignore, read_write_ignore);
 
 	/*
 	 * transient workspace for use while considering a specific join path; T =
@@ -2543,7 +2547,7 @@ typedef struct SpecialJoinInfo SpecialJoinInfo;
 #define HAVE_SPECIALJOININFO_TYPEDEF 1
 #endif
 
-struct SpecialJoinInfo
+struct SpecialJoinInfo pg_node_attr(no_read)
 {
 	NodeTag		type;
 	Relids		min_lefthand;	/* base relids in minimum LHS for join */
@@ -2665,7 +2669,7 @@ typedef struct AppendRelInfo
  * We add such a reference to root->processed_tlist when creating the entry,
  * and it propagates into the plan tree from there.
  */
-typedef struct RowIdentityVarInfo pg_node_attr(no_copy_equal)
+typedef struct RowIdentityVarInfo pg_node_attr(no_copy_equal, no_read)
 {
 	NodeTag		type;
 
@@ -2701,7 +2705,7 @@ typedef struct RowIdentityVarInfo pg_node_attr(no_copy_equal)
  * don't result in unnecessary constraints on join order.
  */
 
-typedef struct PlaceHolderInfo
+typedef struct PlaceHolderInfo pg_node_attr(no_read)
 {
 	NodeTag		type;
 
@@ -2732,7 +2736,7 @@ typedef struct PlaceHolderInfo
  * function.  MinMaxAggPath contains a list of these, and if we accept that
  * path, the list is stored into root->minmax_aggs for use during setrefs.c.
  */
-typedef struct MinMaxAggInfo pg_node_attr(no_copy_equal)
+typedef struct MinMaxAggInfo pg_node_attr(no_copy_equal, no_read)
 {
 	NodeTag		type;
 
@@ -2808,7 +2812,7 @@ typedef struct MinMaxAggInfo pg_node_attr(no_copy_equal)
  * Instead, we just record the assignment of the slot number by appending to
  * root->glob->paramExecTypes.
  */
-typedef struct PlannerParamItem pg_node_attr(no_copy_equal)
+typedef struct PlannerParamItem pg_node_attr(no_copy_equal, no_read)
 {
 	NodeTag		type;
 
diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h
index 19b5ce2ec6..ab7fd8054b 100644
--- a/src/include/nodes/plannodes.h
+++ b/src/include/nodes/plannodes.h
@@ -38,9 +38,12 @@
  * nodes; in such cases, commandType == CMD_UTILITY, the statement itself
  * is in the utilityStmt field, and the rest of the struct is mostly dummy.
  * (We do use canSetTag, stmt_location, stmt_len, and possibly queryId.)
+ *
+ * PlannedStmt, as well as all varieties of Plan, do not support equal(),
+ * not because it's not sensible but because we currently have no need.
  * ----------------
  */
-typedef struct PlannedStmt
+typedef struct PlannedStmt pg_node_attr(no_equal)
 {
 	NodeTag		type;
 
@@ -108,7 +111,7 @@ typedef struct PlannedStmt
  * abstract superclass for all Plan-type nodes.
  * ----------------
  */
-typedef struct Plan
+typedef struct Plan pg_node_attr(abstract, no_equal)
 {
 	NodeTag		type;
 
@@ -725,6 +728,12 @@ typedef struct CustomScan
 	List	   *custom_private; /* private data for custom code */
 	List	   *custom_scan_tlist;	/* optional tlist describing scan tuple */
 	Bitmapset  *custom_relids;	/* RTIs generated by this scan */
+
+	/*
+	 * NOTE: The method field of CustomScan is required to be a pointer to a
+	 * static table of callback functions.  So we don't copy the table itself,
+	 * just reference the original one.
+	 */
 	const struct CustomScanMethods *methods;
 } CustomScan;
 
@@ -756,7 +765,7 @@ typedef struct CustomScan
  * the joinquals, ignoring plan.qual, due to where the executor tests it.)
  * ----------------
  */
-typedef struct Join
+typedef struct Join pg_node_attr(abstract)
 {
 	Plan		plan;
 	JoinType	jointype;
@@ -781,7 +790,7 @@ typedef struct NestLoop
 	List	   *nestParams;		/* list of NestLoopParam nodes */
 } NestLoop;
 
-typedef struct NestLoopParam
+typedef struct NestLoopParam pg_node_attr(no_equal)
 {
 	NodeTag		type;
 	int			paramno;		/* number of the PARAM_EXEC Param to set */
@@ -1343,7 +1352,7 @@ typedef enum RowMarkType
  * Note this means that all tables in an inheritance hierarchy share the
  * same resjunk column names.
  */
-typedef struct PlanRowMark
+typedef struct PlanRowMark pg_node_attr(no_equal)
 {
 	NodeTag		type;
 	Index		rti;			/* range table index of markable relation */
@@ -1387,7 +1396,7 @@ typedef struct PlanRowMark
  *						by any of the PartitionedRelPruneInfo nodes in
  *						"prune_infos".  These subplans must not be pruned.
  */
-typedef struct PartitionPruneInfo
+typedef struct PartitionPruneInfo pg_node_attr(no_equal)
 {
 	NodeTag		type;
 	List	   *prune_infos;
@@ -1411,7 +1420,7 @@ typedef struct PartitionPruneInfo
  * node, but partition indexes are valid only within a particular hierarchy.
  * relid_map[p] contains the partition's OID, or 0 if the partition was pruned.
  */
-typedef struct PartitionedRelPruneInfo
+typedef struct PartitionedRelPruneInfo pg_node_attr(no_equal)
 {
 	NodeTag		type;
 
@@ -1485,7 +1494,7 @@ typedef struct PartitionPruneStep pg_node_attr(abstract)
  * have an expression be present in 'exprs' for a given partition key and
  * the corresponding bit set in 'nullkeys'.
  */
-typedef struct PartitionPruneStepOp
+typedef struct PartitionPruneStepOp pg_node_attr(no_equal)
 {
 	PartitionPruneStep step;
 
@@ -1507,7 +1516,7 @@ typedef enum PartitionPruneCombineOp
 	PARTPRUNE_COMBINE_INTERSECT
 } PartitionPruneCombineOp;
 
-typedef struct PartitionPruneStepCombine
+typedef struct PartitionPruneStepCombine pg_node_attr(no_equal)
 {
 	PartitionPruneStep step;
 
@@ -1525,7 +1534,7 @@ typedef struct PartitionPruneStepCombine
  * to be used with the syscache invalidation mechanism, so it identifies a
  * system catalog entry by cache ID and hash value.
  */
-typedef struct PlanInvalItem
+typedef struct PlanInvalItem pg_node_attr(no_equal)
 {
 	NodeTag		type;
 	int			cacheId;		/* a syscache ID, see utils/syscache.h */
diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h
index 9e6b4bdb1d..6f3dcc74c3 100644
--- a/src/include/nodes/primnodes.h
+++ b/src/include/nodes/primnodes.h
@@ -68,7 +68,7 @@ typedef struct RangeVar
 	 * the catalog (database) name, or NULL; ignored for read/write, since it
 	 * is presently not semantically meaningful
 	 */
-	char	   *catalogname pg_node_attr(read_write_ignore);
+	char	   *catalogname pg_node_attr(read_write_ignore, read_as(NULL));
 
 	/* the schema name, or NULL */
 	char	   *schemaname;
@@ -636,6 +636,7 @@ typedef struct NamedArgExpr
  * Note that opfuncid is not necessarily filled in immediately on creation
  * of the node.  The planner makes sure it is valid before passing the node
  * tree to the executor, but during parsing/planning opfuncid can be 0.
+ * Therefore, equal() will accept a zero value as being equal to other values.
  */
 typedef struct OpExpr
 {
diff --git a/src/include/utils/rel.h b/src/include/utils/rel.h
index 3e6da86688..7b60450b26 100644
--- a/src/include/utils/rel.h
+++ b/src/include/utils/rel.h
@@ -265,7 +265,7 @@ typedef struct RelationData
  * Currently, we mostly cache fields of interest to the planner, but the set
  * of fields has already grown the constraint OID for other uses.
  */
-typedef struct ForeignKeyCacheInfo pg_node_attr(no_read)
+typedef struct ForeignKeyCacheInfo pg_node_attr(no_equal, no_read)
 {
 	NodeTag		type;
 	Oid			conoid;			/* oid of the constraint itself */
diff --git a/src/test/modules/test_oat_hooks/test_oat_hooks.c b/src/test/modules/test_oat_hooks/test_oat_hooks.c
index 7ef272cc7a..b648ee67ff 100644
--- a/src/test/modules/test_oat_hooks/test_oat_hooks.c
+++ b/src/test/modules/test_oat_hooks/test_oat_hooks.c
@@ -468,9 +468,6 @@ nodetag_to_string(NodeTag tag)
 		case T_TupleTableSlot:
 			return "TupleTableSlot";
 			break;
-		case T_Plan:
-			return "Plan";
-			break;
 		case T_Result:
 			return "Result";
 			break;
@@ -549,9 +546,6 @@ nodetag_to_string(NodeTag tag)
 		case T_CustomScan:
 			return "CustomScan";
 			break;
-		case T_Join:
-			return "Join";
-			break;
 		case T_NestLoop:
 			return "NestLoop";
 			break;
