From cdae305960ff955de65c6642cd41ebeaa3995c51 Mon Sep 17 00:00:00 2001
From: Jim Jones <jim.jones@uni-muenster.de>
Date: Sun, 6 Sep 2026 20:27:36 +0200
Subject: [PATCH v3 2/4] Replace pg_class.relispopulated with epoch-capable
 relpopulated

Retype the bool relispopulated to an int64 relpopulated: 0 means no
data, 1 means crash-safe populated (every relation except unlogged
matviews), anything else is an epoch stamp recording when an unlogged
matview was populated.

The read side moves to RelationIsPopulated() in matview.c, replacing
the old rel.h macros. Rename rather than reuse the column so external
queries fail loudly. Add pg_matview_is_populated(regclass), used by
pg_matviews. pg_dump reads relpopulated <> 0 and, in binary upgrade,
calls binary_upgrade_set_matview_populated() instead of poking pg_class.
---
 doc/src/sgml/catalogs.sgml                 | 27 +++++----
 doc/src/sgml/func/func-info.sgml           | 16 ++++++
 src/backend/catalog/heap.c                 |  2 +-
 src/backend/catalog/system_views.sql       |  2 +-
 src/backend/commands/copyto.c              |  1 +
 src/backend/commands/matview.c             | 64 +++++++++++++++++++++-
 src/backend/commands/repack.c              |  1 +
 src/backend/executor/execUtils.c           |  3 +-
 src/backend/utils/adt/pg_upgrade_support.c | 20 +++++++
 src/backend/utils/cache/relcache.c         |  8 +--
 src/bin/pg_dump/pg_dump.c                  | 39 +++++++++----
 src/include/catalog/catversion.h           |  2 +-
 src/include/catalog/pg_class.h             | 18 ++++--
 src/include/catalog/pg_proc.dat            |  8 +++
 src/include/commands/matview.h             |  2 +
 src/include/utils/rel.h                    | 16 ------
 src/test/regress/expected/matview.out      | 31 ++++++++---
 src/test/regress/expected/rules.out        |  2 +-
 src/test/regress/sql/matview.sql           |  9 ++-
 19 files changed, 207 insertions(+), 64 deletions(-)

diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index ecac7332e8e..1eb996d93df 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -2254,16 +2254,6 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
       </para></entry>
      </row>
 
-     <row>
-      <entry role="catalog_table_entry"><para role="column_definition">
-       <structfield>relispopulated</structfield> <type>bool</type>
-      </para>
-      <para>
-       True if relation is populated (this is true for all
-       relations other than some materialized views)
-      </para></entry>
-     </row>
-
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>relreplident</structfield> <type>char</type>
@@ -2327,6 +2317,23 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>relpopulated</structfield> <type>int8</type>
+      </para>
+      <para>
+       Populated epoch.  Zero if the relation does not hold data (a
+       materialized view created or refreshed <literal>WITH NO DATA</literal>).
+       One if the relation is populated with crash-safe storage; this is the
+       value for every relation other than a materialized view, and for
+       populated permanent materialized views.  Any other value is an epoch
+       stamp recording when an unlogged materialized view was populated; its
+       data is valid only while the cluster remains in that epoch.  Use
+       <function>pg_matview_is_populated</function> instead of reading this
+       column directly.
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>relacl</structfield> <type>aclitem[]</type>
diff --git a/doc/src/sgml/func/func-info.sgml b/doc/src/sgml/func/func-info.sgml
index 2f03766b67a..56dc58ba626 100644
--- a/doc/src/sgml/func/func-info.sgml
+++ b/doc/src/sgml/func/func-info.sgml
@@ -1875,6 +1875,22 @@ SELECT currval(pg_get_serial_sequence('sometable', 'id'));
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_matview_is_populated</primary>
+        </indexterm>
+        <function>pg_matview_is_populated</function> ( <type>regclass</type> )
+        <returnvalue>boolean</returnvalue>
+       </para>
+       <para>
+        Returns true if the materialized view currently holds valid data,
+        false if it must be refreshed before use.  Returns
+        <literal>NULL</literal> for arguments that are not materialized
+        views.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/src/backend/catalog/heap.c b/src/backend/catalog/heap.c
index b018f26545b..68f3acea8a3 100644
--- a/src/backend/catalog/heap.c
+++ b/src/backend/catalog/heap.c
@@ -965,12 +965,12 @@ InsertPgClassTuple(Relation pg_class_desc,
 	values[Anum_pg_class_relrowsecurity - 1] = BoolGetDatum(rd_rel->relrowsecurity);
 	values[Anum_pg_class_relforcerowsecurity - 1] = BoolGetDatum(rd_rel->relforcerowsecurity);
 	values[Anum_pg_class_relhassubclass - 1] = BoolGetDatum(rd_rel->relhassubclass);
-	values[Anum_pg_class_relispopulated - 1] = BoolGetDatum(rd_rel->relispopulated);
 	values[Anum_pg_class_relreplident - 1] = CharGetDatum(rd_rel->relreplident);
 	values[Anum_pg_class_relispartition - 1] = BoolGetDatum(rd_rel->relispartition);
 	values[Anum_pg_class_relrewrite - 1] = ObjectIdGetDatum(rd_rel->relrewrite);
 	values[Anum_pg_class_relfrozenxid - 1] = TransactionIdGetDatum(rd_rel->relfrozenxid);
 	values[Anum_pg_class_relminmxid - 1] = MultiXactIdGetDatum(rd_rel->relminmxid);
+	values[Anum_pg_class_relpopulated - 1] = Int64GetDatum(rd_rel->relpopulated);
 	if (relacl != (Datum) 0)
 		values[Anum_pg_class_relacl - 1] = relacl;
 	else
diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index 8612d99a890..be344ff327f 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -150,7 +150,7 @@ CREATE VIEW pg_matviews AS
         pg_get_userbyid(C.relowner) AS matviewowner,
         T.spcname AS tablespace,
         C.relhasindex AS hasindexes,
-        C.relispopulated AS ispopulated,
+        pg_matview_is_populated(C.oid) AS ispopulated,
         pg_get_viewdef(C.oid) AS definition
     FROM pg_class C LEFT JOIN pg_namespace N ON (N.oid = C.relnamespace)
          LEFT JOIN pg_tablespace T ON (T.oid = C.reltablespace)
diff --git a/src/backend/commands/copyto.c b/src/backend/commands/copyto.c
index 5850608a3fb..e84c6719d18 100644
--- a/src/backend/commands/copyto.c
+++ b/src/backend/commands/copyto.c
@@ -23,6 +23,7 @@
 #include "access/tupconvert.h"
 #include "catalog/pg_inherits.h"
 #include "commands/copyapi.h"
+#include "commands/matview.h"
 #include "commands/progress.h"
 #include "executor/execdesc.h"
 #include "executor/executor.h"
diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c
index 40748958eaf..f8c18720e99 100644
--- a/src/backend/commands/matview.c
+++ b/src/backend/commands/matview.c
@@ -20,6 +20,7 @@
 #include "access/multixact.h"
 #include "access/tableam.h"
 #include "access/xact.h"
+#include "access/xlog.h"
 #include "catalog/indexing.h"
 #include "catalog/namespace.h"
 #include "catalog/pg_am.h"
@@ -79,6 +80,7 @@ SetMatViewPopulatedState(Relation relation, bool newstate)
 {
 	Relation	pgrel;
 	HeapTuple	tuple;
+	Form_pg_class classform;
 
 	Assert(relation->rd_rel->relkind == RELKIND_MATVIEW);
 
@@ -94,7 +96,14 @@ SetMatViewPopulatedState(Relation relation, bool newstate)
 		elog(ERROR, "cache lookup failed for relation %u",
 			 RelationGetRelid(relation));
 
-	((Form_pg_class) GETSTRUCT(tuple))->relispopulated = newstate;
+	classform = (Form_pg_class) GETSTRUCT(tuple);
+
+	if (!newstate)
+		classform->relpopulated = RELPOPULATED_NONE;
+	else if (relation->rd_rel->relpersistence == RELPERSISTENCE_UNLOGGED)
+		classform->relpopulated = (int64) GetUnloggedPopulatedEpoch();
+	else
+		classform->relpopulated = RELPOPULATED_ETERNAL;
 
 	CatalogTupleUpdate(pgrel, &tuple->t_self, tuple);
 
@@ -108,6 +117,59 @@ SetMatViewPopulatedState(Relation relation, bool newstate)
 	CommandCounterIncrement();
 }
 
+/*
+ * MatViewPopulatedValueIsValid
+ *		Does this pg_class.relpopulated value denote currently valid data?
+ *
+ * This only distinguishes RELPOPULATED_NONE from everything else; any other
+ * value, whether RELPOPULATED_ETERNAL or an epoch stamp, counts as valid.
+ */
+bool
+MatViewPopulatedValueIsValid(int64 value)
+{
+	return value != RELPOPULATED_NONE;
+}
+
+/*
+ * RelationIsPopulated
+ *		Does this relation currently hold valid data?  Only a materialized
+ *		view can return false.
+ */
+bool
+RelationIsPopulated(Relation relation)
+{
+	return MatViewPopulatedValueIsValid(relation->rd_rel->relpopulated);
+}
+
+/*
+ * pg_matview_is_populated
+ *		Does the materialized view currently hold valid data?
+ *
+ * Returns NULL if the argument is not a materialized view, or if it does
+ * not exist.
+ */
+Datum
+pg_matview_is_populated(PG_FUNCTION_ARGS)
+{
+	Oid			relid = PG_GETARG_OID(0);
+	HeapTuple	tuple;
+	Form_pg_class classform;
+	bool		result;
+
+	tuple = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(tuple))
+		PG_RETURN_NULL();
+	classform = (Form_pg_class) GETSTRUCT(tuple);
+	if (classform->relkind != RELKIND_MATVIEW)
+	{
+		ReleaseSysCache(tuple);
+		PG_RETURN_NULL();
+	}
+	result = MatViewPopulatedValueIsValid(classform->relpopulated);
+	ReleaseSysCache(tuple);
+	PG_RETURN_BOOL(result);
+}
+
 /*
  * ExecRefreshMatView -- execute a REFRESH MATERIALIZED VIEW command
  *
diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index 477c86b2ba6..926057426b1 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -53,6 +53,7 @@
 #include "catalog/pg_inherits.h"
 #include "catalog/toasting.h"
 #include "commands/defrem.h"
+#include "commands/matview.h"
 #include "commands/progress.h"
 #include "commands/repack.h"
 #include "commands/repack_internal.h"
diff --git a/src/backend/executor/execUtils.c b/src/backend/executor/execUtils.c
index c0c4276acbf..3433e29cad8 100644
--- a/src/backend/executor/execUtils.c
+++ b/src/backend/executor/execUtils.c
@@ -49,6 +49,7 @@
 #include "access/table.h"
 #include "access/tableam.h"
 #include "access/tupconvert.h"
+#include "commands/matview.h"
 #include "executor/executor.h"
 #include "executor/nodeModifyTable.h"
 #include "jit/jit.h"
@@ -778,7 +779,7 @@ ExecOpenScanRelation(EState *estate, Index scanrelid, int eflags)
 	 * to do this, perhaps, but there is no better place.
 	 */
 	if ((eflags & (EXEC_FLAG_EXPLAIN_ONLY | EXEC_FLAG_WITH_NO_DATA)) == 0 &&
-		!RelationIsScannable(rel))
+		!RelationIsPopulated(rel))
 		ereport(ERROR,
 				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
 				 errmsg("materialized view \"%s\" has not been populated",
diff --git a/src/backend/utils/adt/pg_upgrade_support.c b/src/backend/utils/adt/pg_upgrade_support.c
index b505a6b4fee..2f2a01e3a49 100644
--- a/src/backend/utils/adt/pg_upgrade_support.c
+++ b/src/backend/utils/adt/pg_upgrade_support.c
@@ -19,6 +19,7 @@
 #include "catalog/pg_subscription_rel.h"
 #include "catalog/pg_type.h"
 #include "commands/extension.h"
+#include "commands/matview.h"
 #include "miscadmin.h"
 #include "replication/logical.h"
 #include "replication/logicallauncher.h"
@@ -29,6 +30,7 @@
 #include "utils/builtins.h"
 #include "utils/lsyscache.h"
 #include "utils/pg_lsn.h"
+#include "utils/rel.h"
 
 
 #define CHECK_IS_BINARY_UPGRADE									\
@@ -181,6 +183,24 @@ binary_upgrade_set_next_pg_authid_oid(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+Datum
+binary_upgrade_set_matview_populated(PG_FUNCTION_ARGS)
+{
+	Oid			relid = PG_GETARG_OID(0);
+	Relation	rel;
+
+	CHECK_IS_BINARY_UPGRADE;
+
+	rel = relation_open(relid, AccessExclusiveLock);
+	if (rel->rd_rel->relkind != RELKIND_MATVIEW)
+		elog(ERROR, "relation \"%s\" is not a materialized view",
+			 RelationGetRelationName(rel));
+	SetMatViewPopulatedState(rel, true);
+	relation_close(rel, NoLock);
+
+	PG_RETURN_VOID();
+}
+
 Datum
 binary_upgrade_create_empty_extension(PG_FUNCTION_ARGS)
 {
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index f475d703977..fa443dc6527 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -1944,7 +1944,7 @@ formrdesc(const char *relationName, Oid relationReltype,
 	relation->rd_rel->relpersistence = RELPERSISTENCE_PERMANENT;
 
 	/* ... and they're always populated, too */
-	relation->rd_rel->relispopulated = true;
+	relation->rd_rel->relpopulated = RELPOPULATED_ETERNAL;
 
 	relation->rd_rel->relreplident = REPLICA_IDENTITY_NOTHING;
 	relation->rd_rel->relpages = 0;
@@ -3668,10 +3668,8 @@ RelationBuildLocalRelation(const char *relname,
 	}
 
 	/* if it's a materialized view, it's not populated initially */
-	if (relkind == RELKIND_MATVIEW)
-		rel->rd_rel->relispopulated = false;
-	else
-		rel->rd_rel->relispopulated = true;
+	rel->rd_rel->relpopulated = (relkind == RELKIND_MATVIEW) ?
+		RELPOPULATED_NONE : RELPOPULATED_ETERNAL;
 
 	/* set replica identity -- system catalogs and non-tables don't have one */
 	if (!IsCatalogNamespace(relnamespace) &&
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index b5b257f9983..483ce625f06 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -7253,8 +7253,26 @@ getTables(Archive *fout, int *numTables)
 		appendPQExpBufferStr(query,
 							 "c.relhasoids, ");
 
-	appendPQExpBufferStr(query,
-						 "c.relispopulated, ");
+	/*
+	 * A normal dump repopulates matviews with REFRESH, so any nonzero
+	 * relpopulated value (the user's intent to have the matview populated)
+	 * should dump as populated.  Binary upgrade instead transfers the heap
+	 * storage as-is, so it must use the effective state: an unlogged matview
+	 * whose epoch stamp went stale (storage reset by a crash and never
+	 * refreshed) has to be restored as unpopulated, or the new cluster would
+	 * present the transferred empty heap as valid data.
+	 */
+	if (fout->remoteVersion >= 190000 && fout->dopt->binary_upgrade)
+		appendPQExpBufferStr(query,
+							 "(CASE WHEN c.relkind = " CppAsString2(RELKIND_MATVIEW)
+							 " THEN pg_catalog.pg_matview_is_populated(c.oid) "
+							 "ELSE true END) AS relispopulated, ");
+	else if (fout->remoteVersion >= 190000)
+		appendPQExpBufferStr(query,
+							 "c.relpopulated <> 0 AS relispopulated, ");
+	else
+		appendPQExpBufferStr(query,
+							 "c.relispopulated, ");
 
 	appendPQExpBufferStr(query,
 						 "c.relreplident, ");
@@ -17812,21 +17830,20 @@ dumpTableSchema(Archive *fout, const TableInfo *tbinfo)
 		}
 
 		/*
-		 * In binary_upgrade mode, restore matviews' populated status by
-		 * poking pg_class directly.  This is pretty ugly, but we can't use
-		 * REFRESH MATERIALIZED VIEW since it's possible that some underlying
-		 * matview is not populated even though this matview is; in any case,
-		 * we want to transfer the matview's heap storage, not run REFRESH.
+		 * In binary_upgrade mode, restore matviews' populated status using
+		 * the binary_upgrade_set_matview_populated() support function.  We
+		 * can't use REFRESH MATERIALIZED VIEW since it's possible that some
+		 * underlying matview is not populated even though this matview is;
+		 * in any case, we want to transfer the matview's heap storage, not
+		 * run REFRESH.
 		 */
 		if (dopt->binary_upgrade && tbinfo->relkind == RELKIND_MATVIEW &&
 			tbinfo->relispopulated)
 		{
 			appendPQExpBufferStr(q, "\n-- For binary upgrade, mark materialized view as populated\n");
-			appendPQExpBufferStr(q, "UPDATE pg_catalog.pg_class\n"
-								 "SET relispopulated = 't'\n"
-								 "WHERE oid = ");
+			appendPQExpBufferStr(q, "SELECT pg_catalog.binary_upgrade_set_matview_populated(");
 			appendStringLiteralAH(q, qualrelname, fout);
-			appendPQExpBufferStr(q, "::pg_catalog.regclass;\n");
+			appendPQExpBufferStr(q, "::pg_catalog.regclass);\n");
 		}
 
 		/*
diff --git a/src/include/catalog/catversion.h b/src/include/catalog/catversion.h
index f11e244899e..6e4b76e7533 100644
--- a/src/include/catalog/catversion.h
+++ b/src/include/catalog/catversion.h
@@ -57,6 +57,6 @@
  */
 
 /*							yyyymmddN */
-#define CATALOG_VERSION_NO	202608271
+#define CATALOG_VERSION_NO	202609061
 
 #endif
diff --git a/src/include/catalog/pg_class.h b/src/include/catalog/pg_class.h
index c4af599dc90..7038ce0d51e 100644
--- a/src/include/catalog/pg_class.h
+++ b/src/include/catalog/pg_class.h
@@ -115,9 +115,6 @@ CATALOG(pg_class,1259,RelationRelationId) BKI_BOOTSTRAP BKI_ROWTYPE_OID(83,Relat
 	/* row security forced for owners or not */
 	bool		relforcerowsecurity BKI_DEFAULT(f);
 
-	/* matview currently holds query results */
-	bool		relispopulated BKI_DEFAULT(t);
-
 	/* see REPLICA_IDENTITY_xxx constants */
 	char		relreplident BKI_DEFAULT(n);
 
@@ -133,6 +130,15 @@ CATALOG(pg_class,1259,RelationRelationId) BKI_BOOTSTRAP BKI_ROWTYPE_OID(83,Relat
 	/* all multixacts in this rel are >= this; it is really a MultiXactId */
 	TransactionId relminmxid BKI_DEFAULT(1);	/* FirstMultiXactId */
 
+	/*
+	 * Populated epoch.  0 (RELPOPULATED_NONE): no data.  1
+	 * (RELPOPULATED_ETERNAL): crash-safe populated, used for every relation
+	 * except unlogged matviews.  Any other value is an epoch stamp for a
+	 * populated unlogged matview, valid only while the cluster stays in that
+	 * epoch.  See MatViewPopulatedValueIsValid().
+	 */
+	int64		relpopulated BKI_DEFAULT(1);
+
 #ifdef CATALOG_VARLEN			/* variable-length fields start here */
 	/* NOTE: These fields are not present in a relcache entry's rd_rel field. */
 	/* access permissions */
@@ -150,7 +156,7 @@ END_CATALOG_STRUCT
 
 /* Size of fixed part of pg_class tuples, not counting var-length fields */
 #define CLASS_TUPLE_SIZE \
-	 (offsetof(FormData_pg_class,relminmxid) + sizeof(TransactionId))
+	 (offsetof(FormData_pg_class,relpopulated) + sizeof(int64))
 
 /* ----------------
  *		Form_pg_class corresponds to a pointer to a tuple with
@@ -180,6 +186,10 @@ MAKE_SYSCACHE(RELNAMENSP, pg_class_relname_nsp_index, 128);
 #define		  RELKIND_PARTITIONED_INDEX 'I' /* partitioned index */
 #define		  RELKIND_PROPGRAPH		  'g'	/* property graph */
 
+/* Reserved values of pg_class.relpopulated; any other value is an epoch. */
+#define		  RELPOPULATED_NONE		  0 /* not populated */
+#define		  RELPOPULATED_ETERNAL	  1 /* populated, storage is crash-safe */
+
 #define		  RELPERSISTENCE_PERMANENT	'p' /* regular table */
 #define		  RELPERSISTENCE_UNLOGGED	'u' /* unlogged permanent table */
 #define		  RELPERSISTENCE_TEMP		't' /* temporary table */
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 6979c7d1161..a5a29434c42 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -12032,6 +12032,10 @@
   proname => 'binary_upgrade_set_next_pg_tablespace_oid', provolatile => 'v',
   proparallel => 'u', prorettype => 'void', proargtypes => 'oid',
   prosrc => 'binary_upgrade_set_next_pg_tablespace_oid' },
+{ oid => '8668', descr => 'for use by pg_upgrade',
+  proname => 'binary_upgrade_set_matview_populated', provolatile => 'v',
+  proparallel => 'u', prorettype => 'void', proargtypes => 'oid',
+  prosrc => 'binary_upgrade_set_matview_populated' },
 { oid => '6312', descr => 'for use by pg_upgrade',
   proname => 'binary_upgrade_check_logical_slot_pending_wal',
   provolatile => 'v', proparallel => 'u', prorettype => 'pg_lsn',
@@ -12400,6 +12404,10 @@
   proname => 'pg_relation_is_publishable', provolatile => 's',
   prorettype => 'bool', proargtypes => 'regclass',
   prosrc => 'pg_relation_is_publishable' },
+{ oid => '8667', descr => 'is materialized view populated',
+  proname => 'pg_matview_is_populated', provolatile => 's',
+  prorettype => 'bool', proargtypes => 'regclass',
+  prosrc => 'pg_matview_is_populated' },
 
 # rls
 { oid => '3298',
diff --git a/src/include/commands/matview.h b/src/include/commands/matview.h
index 738c731c1a9..06096e43601 100644
--- a/src/include/commands/matview.h
+++ b/src/include/commands/matview.h
@@ -22,6 +22,8 @@
 
 
 extern void SetMatViewPopulatedState(Relation relation, bool newstate);
+extern bool MatViewPopulatedValueIsValid(int64 value);
+extern bool RelationIsPopulated(Relation relation);
 
 extern ObjectAddress ExecRefreshMatView(RefreshMatViewStmt *stmt, const char *queryString,
 										QueryCompletion *qc);
diff --git a/src/include/utils/rel.h b/src/include/utils/rel.h
index 41ab4586c6b..a3e2ec89778 100644
--- a/src/include/utils/rel.h
+++ b/src/include/utils/rel.h
@@ -681,22 +681,6 @@ RelationCloseSmgr(Relation relation)
 	 !(relation)->rd_islocaltemp)
 
 
-/*
- * RelationIsScannable
- *		Currently can only be false for a materialized view which has not been
- *		populated by its query.  This is likely to get more complicated later,
- *		so use a macro which looks like a function.
- */
-#define RelationIsScannable(relation) ((relation)->rd_rel->relispopulated)
-
-/*
- * RelationIsPopulated
- *		Currently, we don't physically distinguish the "populated" and
- *		"scannable" properties of matviews, but that may change later.
- *		Hence, use the appropriate one of these macros in code tests.
- */
-#define RelationIsPopulated(relation) ((relation)->rd_rel->relispopulated)
-
 /*
  * RelationIsAccessibleInLogicalDecoding
  *		True if we need to log enough information to have access via
diff --git a/src/test/regress/expected/matview.out b/src/test/regress/expected/matview.out
index 0355720dfc6..7500bf027da 100644
--- a/src/test/regress/expected/matview.out
+++ b/src/test/regress/expected/matview.out
@@ -27,9 +27,9 @@ EXPLAIN (costs off)
 (3 rows)
 
 CREATE MATERIALIZED VIEW mvtest_tm AS SELECT type, sum(amt) AS totamt FROM mvtest_t GROUP BY type WITH NO DATA;
-SELECT relispopulated FROM pg_class WHERE oid = 'mvtest_tm'::regclass;
- relispopulated 
-----------------
+SELECT pg_matview_is_populated('mvtest_tm'::regclass);
+ pg_matview_is_populated 
+-------------------------
  f
 (1 row)
 
@@ -37,12 +37,25 @@ SELECT * FROM mvtest_tm ORDER BY type;
 ERROR:  materialized view "mvtest_tm" has not been populated
 HINT:  Use the REFRESH MATERIALIZED VIEW command.
 REFRESH MATERIALIZED VIEW mvtest_tm;
-SELECT relispopulated FROM pg_class WHERE oid = 'mvtest_tm'::regclass;
- relispopulated 
-----------------
+SELECT pg_matview_is_populated('mvtest_tm'::regclass);
+ pg_matview_is_populated 
+-------------------------
  t
 (1 row)
 
+-- ... but NULL for a relation that is not a materialized view, or nonexistent
+SELECT pg_matview_is_populated('mvtest_t'::regclass);
+ pg_matview_is_populated 
+-------------------------
+ 
+(1 row)
+
+SELECT pg_matview_is_populated(0);
+ pg_matview_is_populated 
+-------------------------
+ 
+(1 row)
+
 CREATE UNIQUE INDEX mvtest_tm_type ON mvtest_tm (type);
 SELECT * FROM mvtest_tm ORDER BY type;
  type | totamt 
@@ -376,9 +389,9 @@ UNION ALL
    FROM mvtest_vt2;
 
 CREATE MATERIALIZED VIEW mv_test3 AS SELECT * FROM mv_test2 WHERE moo = 12345;
-SELECT relispopulated FROM pg_class WHERE oid = 'mv_test3'::regclass;
- relispopulated 
-----------------
+SELECT pg_matview_is_populated('mv_test3'::regclass);
+ pg_matview_is_populated 
+-------------------------
  t
 (1 row)
 
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 1a29d46213e..feca35216b4 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1414,7 +1414,7 @@ pg_matviews| SELECT n.nspname AS schemaname,
     pg_get_userbyid(c.relowner) AS matviewowner,
     t.spcname AS tablespace,
     c.relhasindex AS hasindexes,
-    c.relispopulated AS ispopulated,
+    pg_matview_is_populated((c.oid)::regclass) AS ispopulated,
     pg_get_viewdef(c.oid) AS definition
    FROM ((pg_class c
      LEFT JOIN pg_namespace n ON ((n.oid = c.relnamespace)))
diff --git a/src/test/regress/sql/matview.sql b/src/test/regress/sql/matview.sql
index 934426b9ae8..8890a2f7932 100644
--- a/src/test/regress/sql/matview.sql
+++ b/src/test/regress/sql/matview.sql
@@ -15,10 +15,13 @@ SELECT * FROM mvtest_tv ORDER BY type;
 EXPLAIN (costs off)
   CREATE MATERIALIZED VIEW mvtest_tm AS SELECT type, sum(amt) AS totamt FROM mvtest_t GROUP BY type WITH NO DATA;
 CREATE MATERIALIZED VIEW mvtest_tm AS SELECT type, sum(amt) AS totamt FROM mvtest_t GROUP BY type WITH NO DATA;
-SELECT relispopulated FROM pg_class WHERE oid = 'mvtest_tm'::regclass;
+SELECT pg_matview_is_populated('mvtest_tm'::regclass);
 SELECT * FROM mvtest_tm ORDER BY type;
 REFRESH MATERIALIZED VIEW mvtest_tm;
-SELECT relispopulated FROM pg_class WHERE oid = 'mvtest_tm'::regclass;
+SELECT pg_matview_is_populated('mvtest_tm'::regclass);
+-- ... but NULL for a relation that is not a materialized view, or nonexistent
+SELECT pg_matview_is_populated('mvtest_t'::regclass);
+SELECT pg_matview_is_populated(0);
 CREATE UNIQUE INDEX mvtest_tm_type ON mvtest_tm (type);
 SELECT * FROM mvtest_tm ORDER BY type;
 
@@ -122,7 +125,7 @@ CREATE VIEW mvtest_vt2 AS SELECT moo, 2*moo FROM mvtest_vt1 UNION ALL SELECT moo
 CREATE MATERIALIZED VIEW mv_test2 AS SELECT moo, 2*moo FROM mvtest_vt2 UNION ALL SELECT moo, 3*moo FROM mvtest_vt2;
 \d+ mv_test2
 CREATE MATERIALIZED VIEW mv_test3 AS SELECT * FROM mv_test2 WHERE moo = 12345;
-SELECT relispopulated FROM pg_class WHERE oid = 'mv_test3'::regclass;
+SELECT pg_matview_is_populated('mv_test3'::regclass);
 
 DROP VIEW mvtest_vt1 CASCADE;
 
-- 
2.55.0

