agora inbox for pgsql-hackers@postgresql.org  
help / color / mirror / Atom feed
CREATE OR REPLACE MATERIALIZED VIEW
29+ messages / 11 participants
[nested] [flat]

* CREATE OR REPLACE MATERIALIZED VIEW
@ 2024-07-02 01:22  Erik Wienhold <ewie@ewie.name>
  0 siblings, 3 replies; 29+ messages in thread

From: Erik Wienhold @ 2024-07-02 01:22 UTC (permalink / raw)
  To: pgsql-hackers

I like to add CREATE OR REPLACE MATERIALIZED VIEW with the attached
patches.

Patch 0001 adds CREATE OR REPLACE MATERIALIZED VIEW similar to CREATE OR
REPLACE VIEW.  It also includes regression tests and changes to docs.

Patch 0002 deprecates CREATE MATERIALIZED VIEW IF NOT EXISTS because it
no longer seems necessary with patch 0001.  Tom Lane commented[1] about
the general dislike of IF NOT EXISTS, to which I agree, but maybe this
was meant only in response to adding new commands.  Anyway, my idea is
to deprecate that usage in PG18 and eventually remove it in PG19, if
there's consensus for it.  We can drop that clause without violating any
standard because matviews are a Postgres extension.  I'm not married to
the idea, just want to put it on the table for discussion.

Motivation
----------

At $JOB we use materialized views for caching a couple of expensive
views.  But every now and then those views have to be changed, e.g., new
logic, new columns, etc.  The matviews have to be dropped and re-created
to include new columns.  (Just changing the underlying view logic
without adding new columns is trivial because the matviews are just thin
wrappers that just have to be refreshed.)

We also have several views that depend on those matviews.  The views
must also be dropped in order to re-create the matviews.  We've already
automated this with two procedures that stash and re-create dependent
view definitions.

Native support for replacing matviews would simplify our setup and it
would make CREATE MATERIALIZED VIEW more complete when compared to
CREATE VIEW.

I searched the lists for previous discussions on this topic but couldn't
find any.  So, I don't know if this was ever tried, but rejected for
some reason.  I've found slides[2] from 2013 (when matviews landed in
9.3) which have OR REPLACE on the roadmap:

> Materialised Views roadmap
>
> * CREATE **OR REPLACE** MATERIALIZED VIEW
>   * Just an oversight that it wasn't added
>  [...]

Replacing Matviews
------------------

With patch 0001, a matview can be replaced without having to drop it and
its dependent objects.  In our use case it is no longer necessary to
define the actual query in a separate view.  Replacing a matview works
analogous to CREATE OR REPLACE VIEW:

* the new query may change SELECT list expressions of existing columns
* new columns can be added to the end of the SELECT list
* existing columns cannot be renamed
* the data type of existing columns cannot be changed

In addition to that, CREATE OR REPLACE MATERIALIZED VIEW also replaces
access method, tablespace, and storage parameters if specified.  The
clause WITH [NO] DATA works as expected: it either populates the matview
or leaves it in an unscannable state.

It is an error to specify both OR REPLACE and IF NOT EXISTS.

Example
-------

postgres=# CREATE MATERIALIZED VIEW test AS SELECT 1 AS a;
SELECT 1
postgres=# SELECT * FROM test;
 a
---
 1
(1 row)

postgres=# CREATE OR REPLACE MATERIALIZED VIEW test AS SELECT 2 AS a, 3 AS b;
CREATE MATERIALIZED VIEW
postgres=# SELECT * FROM test;
 a | b
---+---
 2 | 3
(1 row)

Implementation Details
----------------------

Patch 0001 extends create_ctas_internal in order to adapt an existing
matview to the new tuple descriptor, access method, tablespace, and
storage parameters.  This logic is mostly based on DefineViewRelation.
This also reuses checkViewColumns, but adds argument is_matview in order
to tell if we want error messages for a matview (true) or view (false).
I'm not sure if that flag is the correct way to do that, or if I should
just create a separate function just for matviews with the same logic.
Do we even need to distinguish between view and matview in those error
messages?

The patch also adds tab completion in psql for CREATE OR REPLACE
MATERIALIZED VIEW.

[1] https://www.postgresql.org/message-id/226806.1693430777%40sss.pgh.pa.us
[2] https://wiki.postgresql.org/images/a/ad/Materialised_views_now_and_the_future-pgconfeu_2013.pdf#page...

-- 
Erik

Attachments:

  [text/x-diff] v1-0001-Add-CREATE-OR-REPLACE-MATERIALIZED-VIEW.patch (30.8K, ../../3c86a16f-4272-4df3-9959-70a9a7d88a71@ewie.name/2-v1-0001-Add-CREATE-OR-REPLACE-MATERIALIZED-VIEW.patch)
  download | inline diff:
From b9f96d4a8e806389bf33a96be6db3a57bccb48cf Mon Sep 17 00:00:00 2001
From: Erik Wienhold <ewie@ewie.name>
Date: Tue, 21 May 2024 18:35:47 +0200
Subject: [PATCH v1 1/2] Add CREATE OR REPLACE MATERIALIZED VIEW

---
 .../sgml/ref/create_materialized_view.sgml    |  15 +-
 src/backend/commands/createas.c               | 207 ++++++++++++++----
 src/backend/commands/tablecmds.c              |   8 +-
 src/backend/commands/view.c                   | 106 ++++++---
 src/backend/parser/gram.y                     |  15 ++
 src/bin/psql/tab-complete.c                   |   2 +-
 src/include/commands/view.h                   |   3 +
 src/include/nodes/parsenodes.h                |   2 +-
 src/include/nodes/primnodes.h                 |   1 +
 src/test/regress/expected/matview.out         | 191 ++++++++++++++++
 src/test/regress/sql/matview.sql              | 108 +++++++++
 11 files changed, 574 insertions(+), 84 deletions(-)

diff --git a/doc/src/sgml/ref/create_materialized_view.sgml b/doc/src/sgml/ref/create_materialized_view.sgml
index 0d2fea2b97..b5a8e3441a 100644
--- a/doc/src/sgml/ref/create_materialized_view.sgml
+++ b/doc/src/sgml/ref/create_materialized_view.sgml
@@ -21,7 +21,7 @@ PostgreSQL documentation
 
  <refsynopsisdiv>
 <synopsis>
-CREATE MATERIALIZED VIEW [ IF NOT EXISTS ] <replaceable>table_name</replaceable>
+CREATE [ OR REPLACE ] MATERIALIZED VIEW [ IF NOT EXISTS ] <replaceable>table_name</replaceable>
     [ (<replaceable>column_name</replaceable> [, ...] ) ]
     [ USING <replaceable class="parameter">method</replaceable> ]
     [ WITH ( <replaceable class="parameter">storage_parameter</replaceable> [= <replaceable class="parameter">value</replaceable>] [, ... ] ) ]
@@ -60,6 +60,17 @@ CREATE MATERIALIZED VIEW [ IF NOT EXISTS ] <replaceable>table_name</replaceable>
   <title>Parameters</title>
 
   <variablelist>
+   <varlistentry>
+    <term><literal>OR REPLACE</literal></term>
+    <listitem>
+     <para>
+      Replaces a materialized view if it already exists.
+      Specifying <literal>OR REPLACE</literal> together with
+      <literal>IF NOT EXISTS</literal> is an error.
+     </para>
+    </listitem>
+   </varlistentry>
+
    <varlistentry>
     <term><literal>IF NOT EXISTS</literal></term>
     <listitem>
@@ -67,7 +78,7 @@ CREATE MATERIALIZED VIEW [ IF NOT EXISTS ] <replaceable>table_name</replaceable>
       Do not throw an error if a materialized view with the same name already
       exists. A notice is issued in this case.  Note that there is no guarantee
       that the existing materialized view is anything like the one that would
-      have been created.
+      have been created, unless you use <literal>OR REPLACE</literal> instead.
      </para>
     </listitem>
    </varlistentry>
diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c
index 62050f4dc5..1f34665521 100644
--- a/src/backend/commands/createas.c
+++ b/src/backend/commands/createas.c
@@ -79,55 +79,151 @@ static void intorel_destroy(DestReceiver *self);
 static ObjectAddress
 create_ctas_internal(List *attrList, IntoClause *into)
 {
-	CreateStmt *create = makeNode(CreateStmt);
-	bool		is_matview;
+	bool		is_matview,
+				replace = false;
 	char		relkind;
-	Datum		toast_options;
-	static char *validnsps[] = HEAP_RELOPT_NAMESPACES;
+	Oid			matviewOid = InvalidOid;
 	ObjectAddress intoRelationAddr;
 
 	/* This code supports both CREATE TABLE AS and CREATE MATERIALIZED VIEW */
 	is_matview = (into->viewQuery != NULL);
 	relkind = is_matview ? RELKIND_MATVIEW : RELKIND_RELATION;
 
-	/*
-	 * Create the target relation by faking up a CREATE TABLE parsetree and
-	 * passing it to DefineRelation.
-	 */
-	create->relation = into->rel;
-	create->tableElts = attrList;
-	create->inhRelations = NIL;
-	create->ofTypename = NULL;
-	create->constraints = NIL;
-	create->options = into->options;
-	create->oncommit = into->onCommit;
-	create->tablespacename = into->tableSpaceName;
-	create->if_not_exists = false;
-	create->accessMethod = into->accessMethod;
+	/* Check if an existing materialized view needs to be replaced. */
+	if (is_matview)
+	{
+		LOCKMODE	lockmode;
 
-	/*
-	 * Create the relation.  (This will error out if there's an existing view,
-	 * so we don't need more code to complain if "replace" is false.)
-	 */
-	intoRelationAddr = DefineRelation(create, relkind, InvalidOid, NULL, NULL);
+		lockmode = into->replace ? AccessExclusiveLock : NoLock;
+		(void) RangeVarGetAndCheckCreationNamespace(into->rel, lockmode,
+													&matviewOid);
+		replace = OidIsValid(matviewOid) && into->replace;
+	}
 
-	/*
-	 * If necessary, create a TOAST table for the target table.  Note that
-	 * NewRelationCreateToastTable ends with CommandCounterIncrement(), so
-	 * that the TOAST table will be visible for insertion.
-	 */
-	CommandCounterIncrement();
+	if (is_matview && replace)
+	{
+		Relation	rel;
+		List	   *atcmds = NIL;
+		AlterTableCmd *atcmd;
+		TupleDesc	descriptor;
+
+		rel = relation_open(matviewOid, NoLock);
+
+		if (rel->rd_rel->relkind != RELKIND_MATVIEW)
+			ereport(ERROR,
+					errcode(ERRCODE_WRONG_OBJECT_TYPE),
+					errmsg("\"%s\" is not a materialized view",
+						   RelationGetRelationName(rel)));
+
+		CheckTableNotInUse(rel, "CREATE OR REPLACE MATERIALIZED VIEW");
+
+		descriptor = BuildDescForRelation(attrList);
+		checkViewColumns(descriptor, rel->rd_att, true);
+
+		/* Add new attributes via ALTER TABLE. */
+		if (list_length(attrList) > rel->rd_att->natts)
+		{
+			ListCell   *c;
+			int			skip = rel->rd_att->natts;
+
+			foreach(c, attrList)
+			{
+				if (skip > 0)
+				{
+					skip--;
+					continue;
+				}
+				atcmd = makeNode(AlterTableCmd);
+				atcmd->subtype = AT_AddColumnToView;
+				atcmd->def = (Node *) lfirst(c);
+				atcmds = lappend(atcmds, atcmd);
+			}
+		}
+
+		/* Set access method via ALTER TABLE. */
+		if (into->accessMethod != NULL)
+		{
+			atcmd = makeNode(AlterTableCmd);
+			atcmd->subtype = AT_SetAccessMethod;
+			atcmd->name = into->accessMethod;
+			atcmds = lappend(atcmds, atcmd);
+		}
+
+		/* Set tablespace via ALTER TABLE. */
+		if (into->tableSpaceName != NULL)
+		{
+			atcmd = makeNode(AlterTableCmd);
+			atcmd->subtype = AT_SetTableSpace;
+			atcmd->name = into->tableSpaceName;
+			atcmds = lappend(atcmds, atcmd);
+		}
+
+		/* Set storage parameters via ALTER TABLE. */
+		if (into->options != NIL)
+		{
+			atcmd = makeNode(AlterTableCmd);
+			atcmd->subtype = AT_ReplaceRelOptions;
+			atcmd->def = (Node *) into->options;
+			atcmds = lappend(atcmds, atcmd);
+		}
+
+		if (atcmds != NIL)
+		{
+			AlterTableInternal(matviewOid, atcmds, true);
+			CommandCounterIncrement();
+		}
+
+		relation_close(rel, NoLock);
+
+		ObjectAddressSet(intoRelationAddr, RelationRelationId, matviewOid);
+	}
+	else
+	{
+		CreateStmt *create = makeNode(CreateStmt);
+		Datum		toast_options;
+		static char *validnsps[] = HEAP_RELOPT_NAMESPACES;
+
+		/*
+		 * Create the target relation by faking up a CREATE TABLE parsetree
+		 * and passing it to DefineRelation.
+		 */
+		create->relation = into->rel;
+		create->tableElts = attrList;
+		create->inhRelations = NIL;
+		create->ofTypename = NULL;
+		create->constraints = NIL;
+		create->options = into->options;
+		create->oncommit = into->onCommit;
+		create->tablespacename = into->tableSpaceName;
+		create->if_not_exists = false;
+		create->accessMethod = into->accessMethod;
+
+		/*
+		 * Create the relation.  (This will error out if there's an existing
+		 * view, so we don't need more code to complain if "replace" is
+		 * false.)
+		 */
+		intoRelationAddr = DefineRelation(create, relkind, InvalidOid, NULL,
+										  NULL);
 
-	/* parse and validate reloptions for the toast table */
-	toast_options = transformRelOptions((Datum) 0,
-										create->options,
-										"toast",
-										validnsps,
-										true, false);
+		/*
+		 * If necessary, create a TOAST table for the target table.  Note that
+		 * NewRelationCreateToastTable ends with CommandCounterIncrement(), so
+		 * that the TOAST table will be visible for insertion.
+		 */
+		CommandCounterIncrement();
+
+		/* parse and validate reloptions for the toast table */
+		toast_options = transformRelOptions((Datum) 0,
+											create->options,
+											"toast",
+											validnsps,
+											true, false);
 
-	(void) heap_reloptions(RELKIND_TOASTVALUE, toast_options, true);
+		(void) heap_reloptions(RELKIND_TOASTVALUE, toast_options, true);
 
-	NewRelationCreateToastTable(intoRelationAddr.objectId, toast_options);
+		NewRelationCreateToastTable(intoRelationAddr.objectId, toast_options);
+	}
 
 	/* Create the "view" part of a materialized view. */
 	if (is_matview)
@@ -135,7 +231,7 @@ create_ctas_internal(List *attrList, IntoClause *into)
 		/* StoreViewQuery scribbles on tree, so make a copy */
 		Query	   *query = (Query *) copyObject(into->viewQuery);
 
-		StoreViewQuery(intoRelationAddr.objectId, query, false);
+		StoreViewQuery(intoRelationAddr.objectId, query, replace);
 		CommandCounterIncrement();
 	}
 
@@ -236,7 +332,26 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt,
 
 	/* Check if the relation exists or not */
 	if (CreateTableAsRelExists(stmt))
+	{
+		/* An existing materialized view can be replaced. */
+		if (is_matview && into->replace)
+		{
+			RefreshMatViewStmt *refresh;
+
+			/* Change the relation to match the new query and other options. */
+			(void) create_ctas_nodata(query->targetList, into);
+
+			/* Refresh the materialized view with a fake statement. */
+			refresh = makeNode(RefreshMatViewStmt);
+			refresh->relation = into->rel;
+			refresh->skipData = into->skipData;
+			refresh->concurrent = false;
+
+			return ExecRefreshMatView(refresh, NULL, NULL, NULL);
+		}
+
 		return InvalidObjectAddress;
+	}
 
 	/*
 	 * Create the tuple receiver object and insert info it will need
@@ -397,14 +512,15 @@ CreateTableAsRelExists(CreateTableAsStmt *ctas)
 	oldrelid = get_relname_relid(into->rel->relname, nspid);
 	if (OidIsValid(oldrelid))
 	{
-		if (!ctas->if_not_exists)
+		if (!ctas->if_not_exists && !into->replace)
 			ereport(ERROR,
 					(errcode(ERRCODE_DUPLICATE_TABLE),
 					 errmsg("relation \"%s\" already exists",
 							into->rel->relname)));
 
 		/*
-		 * The relation exists and IF NOT EXISTS has been specified.
+		 * The relation exists and IF NOT EXISTS or OR REPLACE has been
+		 * specified.
 		 *
 		 * If we are in an extension script, insist that the pre-existing
 		 * object be a member of the extension, to avoid security risks.
@@ -412,11 +528,12 @@ CreateTableAsRelExists(CreateTableAsStmt *ctas)
 		ObjectAddressSet(address, RelationRelationId, oldrelid);
 		checkMembershipInCurrentExtension(&address);
 
-		/* OK to skip */
-		ereport(NOTICE,
-				(errcode(ERRCODE_DUPLICATE_TABLE),
-				 errmsg("relation \"%s\" already exists, skipping",
-						into->rel->relname)));
+		if (ctas->if_not_exists)
+			/* OK to skip */
+			ereport(NOTICE,
+					(errcode(ERRCODE_DUPLICATE_TABLE),
+					 errmsg("relation \"%s\" already exists, skipping",
+							into->rel->relname)));
 		return true;
 	}
 
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 9e9dc5c2c1..4660aa1230 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -4484,7 +4484,7 @@ AlterTableGetLockLevel(List *cmds)
 				 * Subcommands that may be visible to concurrent SELECTs
 				 */
 			case AT_DropColumn: /* change visible to SELECT */
-			case AT_AddColumnToView:	/* CREATE VIEW */
+			case AT_AddColumnToView:	/* via CREATE OR REPLACE [MATERIALIZED] VIEW */
 			case AT_DropOids:	/* used to equiv to DropColumn */
 			case AT_EnableAlwaysRule:	/* may change SELECT rules */
 			case AT_EnableReplicaRule:	/* may change SELECT rules */
@@ -4795,8 +4795,8 @@ ATPrepCmd(List **wqueue, Relation rel, AlterTableCmd *cmd,
 			/* Recursion occurs during execution phase */
 			pass = AT_PASS_ADD_COL;
 			break;
-		case AT_AddColumnToView:	/* add column via CREATE OR REPLACE VIEW */
-			ATSimplePermissions(cmd->subtype, rel, ATT_VIEW);
+		case AT_AddColumnToView:	/* via CREATE OR REPLACE [MATERIALIZED] VIEW */
+			ATSimplePermissions(cmd->subtype, rel, ATT_VIEW | ATT_MATVIEW);
 			ATPrepAddColumn(wqueue, rel, recurse, recursing, true, cmd,
 							lockmode, context);
 			/* Recursion occurs during execution phase */
@@ -5220,7 +5220,7 @@ ATExecCmd(List **wqueue, AlteredTableInfo *tab,
 	switch (cmd->subtype)
 	{
 		case AT_AddColumn:		/* ADD COLUMN */
-		case AT_AddColumnToView:	/* add column via CREATE OR REPLACE VIEW */
+		case AT_AddColumnToView:	/* via CREATE OR REPLACE [MATERIALIZED] VIEW */
 			address = ATExecAddColumn(wqueue, tab, rel, &cmd,
 									  cmd->recurse, false,
 									  lockmode, cur_pass, context);
diff --git a/src/backend/commands/view.c b/src/backend/commands/view.c
index fdad833832..76532aa35d 100644
--- a/src/backend/commands/view.c
+++ b/src/backend/commands/view.c
@@ -30,8 +30,6 @@
 #include "utils/lsyscache.h"
 #include "utils/rel.h"
 
-static void checkViewColumns(TupleDesc newdesc, TupleDesc olddesc);
-
 /*---------------------------------------------------------------------
  * DefineVirtualRelation
  *
@@ -130,7 +128,7 @@ DefineVirtualRelation(RangeVar *relation, List *tlist, bool replace,
 		 * column list.
 		 */
 		descriptor = BuildDescForRelation(attrList);
-		checkViewColumns(descriptor, rel->rd_att);
+		checkViewColumns(descriptor, rel->rd_att, false);
 
 		/*
 		 * If new attributes have been added, we must add pg_attribute entries
@@ -263,15 +261,22 @@ DefineVirtualRelation(RangeVar *relation, List *tlist, bool replace,
  * added to generate specific complaints.  Also, we allow the new view to have
  * more columns than the old.
  */
-static void
-checkViewColumns(TupleDesc newdesc, TupleDesc olddesc)
+void
+checkViewColumns(TupleDesc newdesc, TupleDesc olddesc, bool is_matview)
 {
 	int			i;
 
 	if (newdesc->natts < olddesc->natts)
-		ereport(ERROR,
-				(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
-				 errmsg("cannot drop columns from view")));
+	{
+		if (is_matview)
+			ereport(ERROR,
+					errcode(ERRCODE_INVALID_TABLE_DEFINITION),
+					errmsg("cannot drop columns from materialized view"));
+		else
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
+					 errmsg("cannot drop columns from view")));
+	}
 
 	for (i = 0; i < olddesc->natts; i++)
 	{
@@ -280,17 +285,34 @@ checkViewColumns(TupleDesc newdesc, TupleDesc olddesc)
 
 		/* XXX msg not right, but we don't support DROP COL on view anyway */
 		if (newattr->attisdropped != oldattr->attisdropped)
-			ereport(ERROR,
-					(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
-					 errmsg("cannot drop columns from view")));
+		{
+			if (is_matview)
+				ereport(ERROR,
+						errcode(ERRCODE_INVALID_TABLE_DEFINITION),
+						errmsg("cannot drop columns from materialized view"));
+			else
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
+						 errmsg("cannot drop columns from view")));
+		}
 
 		if (strcmp(NameStr(newattr->attname), NameStr(oldattr->attname)) != 0)
-			ereport(ERROR,
-					(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
-					 errmsg("cannot change name of view column \"%s\" to \"%s\"",
-							NameStr(oldattr->attname),
-							NameStr(newattr->attname)),
-					 errhint("Use ALTER VIEW ... RENAME COLUMN ... to change name of view column instead.")));
+		{
+			if (is_matview)
+				ereport(ERROR,
+						errcode(ERRCODE_INVALID_TABLE_DEFINITION),
+						errmsg("cannot change name of materialized view column \"%s\" to \"%s\"",
+							   NameStr(oldattr->attname),
+							   NameStr(newattr->attname)),
+						errhint("Use ALTER MATERIALIZED VIEW ... RENAME COLUMN ... to change name of materialized view column instead."));
+			else
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
+						 errmsg("cannot change name of view column \"%s\" to \"%s\"",
+								NameStr(oldattr->attname),
+								NameStr(newattr->attname)),
+						 errhint("Use ALTER VIEW ... RENAME COLUMN ... to change name of view column instead.")));
+		}
 
 		/*
 		 * We cannot allow type, typmod, or collation to change, since these
@@ -299,26 +321,48 @@ checkViewColumns(TupleDesc newdesc, TupleDesc olddesc)
 		 */
 		if (newattr->atttypid != oldattr->atttypid ||
 			newattr->atttypmod != oldattr->atttypmod)
-			ereport(ERROR,
-					(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
-					 errmsg("cannot change data type of view column \"%s\" from %s to %s",
-							NameStr(oldattr->attname),
-							format_type_with_typemod(oldattr->atttypid,
-													 oldattr->atttypmod),
-							format_type_with_typemod(newattr->atttypid,
-													 newattr->atttypmod))));
+		{
+			if (is_matview)
+				ereport(ERROR,
+						errcode(ERRCODE_INVALID_TABLE_DEFINITION),
+						errmsg("cannot change data type of materialized view column \"%s\" from %s to %s",
+							   NameStr(oldattr->attname),
+							   format_type_with_typemod(oldattr->atttypid,
+														oldattr->atttypmod),
+							   format_type_with_typemod(newattr->atttypid,
+														newattr->atttypmod)));
+			else
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
+						 errmsg("cannot change data type of view column \"%s\" from %s to %s",
+								NameStr(oldattr->attname),
+								format_type_with_typemod(oldattr->atttypid,
+														 oldattr->atttypmod),
+								format_type_with_typemod(newattr->atttypid,
+														 newattr->atttypmod))));
+		}
 
 		/*
 		 * At this point, attcollations should be both valid or both invalid,
 		 * so applying get_collation_name unconditionally should be fine.
 		 */
 		if (newattr->attcollation != oldattr->attcollation)
-			ereport(ERROR,
-					(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
-					 errmsg("cannot change collation of view column \"%s\" from \"%s\" to \"%s\"",
-							NameStr(oldattr->attname),
-							get_collation_name(oldattr->attcollation),
-							get_collation_name(newattr->attcollation))));
+		{
+			if (is_matview)
+				ereport(ERROR,
+						errcode(ERRCODE_INVALID_TABLE_DEFINITION),
+						errmsg("cannot change collation of materialized view column \"%s\" from \"%s\" to \"%s\"",
+							   NameStr(oldattr->attname),
+							   get_collation_name(oldattr->attcollation),
+							   get_collation_name(newattr->attcollation)));
+			else
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
+						 errmsg("cannot change collation of view column \"%s\" from \"%s\" to \"%s\"",
+								NameStr(oldattr->attname),
+								get_collation_name(oldattr->attcollation),
+								get_collation_name(newattr->attcollation))));
+		}
 	}
 
 	/*
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index a043fd4c66..ed477806d1 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -4831,6 +4831,21 @@ CreateMatViewStmt:
 					$8->skipData = !($11);
 					$$ = (Node *) ctas;
 				}
+		| CREATE OR REPLACE OptNoLog MATERIALIZED VIEW create_mv_target AS SelectStmt opt_with_data
+				{
+					CreateTableAsStmt *ctas = makeNode(CreateTableAsStmt);
+
+					ctas->query = $9;
+					ctas->into = $7;
+					ctas->objtype = OBJECT_MATVIEW;
+					ctas->is_select_into = false;
+					ctas->if_not_exists = false;
+					/* cram additional flags into the IntoClause */
+					$7->rel->relpersistence = $4;
+					$7->skipData = !($10);
+					$7->replace = true;
+					$$ = (Node *) ctas;
+				}
 		;
 
 create_mv_target:
diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index d453e224d9..f4d7f99bc5 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -1811,7 +1811,7 @@ psql_completion(const char *text, int start, int end)
 	/* complete with something you can create or replace */
 	else if (TailMatches("CREATE", "OR", "REPLACE"))
 		COMPLETE_WITH("FUNCTION", "PROCEDURE", "LANGUAGE", "RULE", "VIEW",
-					  "AGGREGATE", "TRANSFORM", "TRIGGER");
+					  "AGGREGATE", "TRANSFORM", "TRIGGER", "MATERIALIZED VIEW");
 
 /* DROP, but not DROP embedded in other commands */
 	/* complete with something you can drop */
diff --git a/src/include/commands/view.h b/src/include/commands/view.h
index d2d8588989..7eacdaaceb 100644
--- a/src/include/commands/view.h
+++ b/src/include/commands/view.h
@@ -22,4 +22,7 @@ extern ObjectAddress DefineView(ViewStmt *stmt, const char *queryString,
 
 extern void StoreViewQuery(Oid viewOid, Query *viewParse, bool replace);
 
+extern void checkViewColumns(TupleDesc newdesc, TupleDesc olddesc,
+							 bool is_matview);
+
 #endif							/* VIEW_H */
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 85a62b538e..cbc4b608e3 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -2351,7 +2351,7 @@ typedef struct AlterTableStmt
 typedef enum AlterTableType
 {
 	AT_AddColumn,				/* add column */
-	AT_AddColumnToView,			/* implicitly via CREATE OR REPLACE VIEW */
+	AT_AddColumnToView,			/* implicitly via CREATE OR REPLACE [MATERIALIZED] VIEW */
 	AT_ColumnDefault,			/* alter column default */
 	AT_CookedColumnDefault,		/* add a pre-cooked column default */
 	AT_DropNotNull,				/* alter column drop not null */
diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h
index ea47652adb..4b0ee5d10d 100644
--- a/src/include/nodes/primnodes.h
+++ b/src/include/nodes/primnodes.h
@@ -168,6 +168,7 @@ typedef struct IntoClause
 	/* materialized view's SELECT query */
 	Node	   *viewQuery pg_node_attr(query_jumble_ignore);
 	bool		skipData;		/* true for WITH NO DATA */
+	bool		replace;		/* replace existing matview? */
 } IntoClause;
 
 
diff --git a/src/test/regress/expected/matview.out b/src/test/regress/expected/matview.out
index 038ab73517..e2e2a13396 100644
--- a/src/test/regress/expected/matview.out
+++ b/src/test/regress/expected/matview.out
@@ -694,3 +694,194 @@ NOTICE:  relation "matview_ine_tab" already exists, skipping
 (0 rows)
 
 DROP MATERIALIZED VIEW matview_ine_tab;
+--
+-- test CREATE OR REPLACE MATERIALIZED VIEW
+--
+-- matview does not already exist
+DROP MATERIALIZED VIEW IF EXISTS mvtest_replace;
+NOTICE:  materialized view "mvtest_replace" does not exist, skipping
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 1 AS a;
+SELECT * FROM mvtest_replace;
+ a 
+---
+ 1
+(1 row)
+
+-- replace query with data
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 2 AS a;
+SELECT * FROM mvtest_replace;
+ a 
+---
+ 2
+(1 row)
+
+-- replace query without data
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 3 AS a
+  WITH NO DATA;
+SELECT * FROM mvtest_replace; -- error: not populated
+ERROR:  materialized view "mvtest_replace" has not been populated
+HINT:  Use the REFRESH MATERIALIZED VIEW command.
+REFRESH MATERIALIZED VIEW mvtest_replace;
+SELECT * FROM mvtest_replace;
+ a 
+---
+ 3
+(1 row)
+
+-- add column
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 4 AS a, 1 b;
+SELECT * FROM mvtest_replace;
+ a | b 
+---+---
+ 4 | 1
+(1 row)
+
+-- replace table options
+SELECT m.*, c.relname, c.reloptions, s.spcname, a.amname
+  FROM mvtest_replace m
+    CROSS JOIN pg_class c
+    LEFT JOIN pg_tablespace s ON s.oid = c.reltablespace
+    LEFT JOIN pg_am a ON a.oid = c.relam
+  WHERE c.relname = 'mvtest_replace';
+ a | b |    relname     | reloptions | spcname | amname 
+---+---+----------------+------------+---------+--------
+ 4 | 1 | mvtest_replace |            |         | heap
+(1 row)
+
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace
+  USING heap2
+  WITH (fillfactor = 50)
+  TABLESPACE regress_tblspace
+  AS SELECT 5 AS a, 1 AS b;
+SELECT m.*, c.relname, c.reloptions, s.spcname, a.amname
+  FROM mvtest_replace m
+    CROSS JOIN pg_class c
+    LEFT JOIN pg_tablespace s ON s.oid = c.reltablespace
+    LEFT JOIN pg_am a ON a.oid = c.relam
+  WHERE c.relname = 'mvtest_replace';
+ a | b |    relname     |   reloptions    |     spcname      | amname 
+---+---+----------------+-----------------+------------------+--------
+ 5 | 1 | mvtest_replace | {fillfactor=50} | regress_tblspace | heap2
+(1 row)
+
+-- can replace matview that has a dependent view
+CREATE VIEW mvtest_replace_v AS
+  SELECT * FROM mvtest_replace;
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 6 AS a, 1 AS b;
+SELECT * FROM mvtest_replace, mvtest_replace_v;
+ a | b | a | b 
+---+---+---+---
+ 6 | 1 | 6 | 1
+(1 row)
+
+DROP VIEW mvtest_replace_v;
+-- index gets rebuilt when replacing with data
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 7 AS a, 1 AS b;
+CREATE UNIQUE INDEX ON mvtest_replace (b);
+SELECT * FROM mvtest_replace;
+ a | b 
+---+---
+ 7 | 1
+(1 row)
+
+SET enable_seqscan = off; -- force index scan
+EXPLAIN (COSTS OFF) SELECT * FROM mvtest_replace WHERE b = 1;
+                       QUERY PLAN                        
+---------------------------------------------------------
+ Index Scan using mvtest_replace_b_idx on mvtest_replace
+   Index Cond: (b = 1)
+(2 rows)
+
+SELECT * FROM mvtest_replace WHERE b = 1;
+ a | b 
+---+---
+ 7 | 1
+(1 row)
+
+RESET enable_seqscan;
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 8 AS a, 1 AS b;
+SET enable_seqscan = off; -- force index scan
+EXPLAIN (COSTS OFF) SELECT * FROM mvtest_replace WHERE b = 1;
+                       QUERY PLAN                        
+---------------------------------------------------------
+ Index Scan using mvtest_replace_b_idx on mvtest_replace
+   Index Cond: (b = 1)
+(2 rows)
+
+SELECT * FROM mvtest_replace WHERE b = 1;
+ a | b 
+---+---
+ 8 | 1
+(1 row)
+
+RESET enable_seqscan;
+-- cannot change column data type
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 9 AS a, 'x' AS b; -- error
+ERROR:  cannot change data type of materialized view column "b" from integer to text
+SELECT * FROM mvtest_replace;
+ a | b 
+---+---
+ 8 | 1
+(1 row)
+
+-- cannot rename column
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 10 AS a, 1 AS b2; -- error
+ERROR:  cannot change name of materialized view column "b" to "b2"
+HINT:  Use ALTER MATERIALIZED VIEW ... RENAME COLUMN ... to change name of materialized view column instead.
+SELECT * FROM mvtest_replace;
+ a | b 
+---+---
+ 8 | 1
+(1 row)
+
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 11 AS a, 1 AS b, 'y' COLLATE "C" AS c;
+SELECT * FROM mvtest_replace;
+ a  | b | c 
+----+---+---
+ 11 | 1 | y
+(1 row)
+
+-- cannot change column collation
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 12 AS a, 1 AS b, 'x' COLLATE "POSIX" AS c; -- error
+ERROR:  cannot change collation of materialized view column "c" from "C" to "POSIX"
+SELECT * FROM mvtest_replace;
+ a  | b | c 
+----+---+---
+ 11 | 1 | y
+(1 row)
+
+-- cannot drop column
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 13 AS a, 1 AS b; -- error
+ERROR:  cannot drop columns from materialized view
+SELECT * FROM mvtest_replace;
+ a  | b | c 
+----+---+---
+ 11 | 1 | y
+(1 row)
+
+-- must target a matview
+CREATE VIEW mvtest_not_mv AS
+  SELECT 1 AS a;
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_not_mv AS
+  SELECT 1 AS a; -- error
+ERROR:  "mvtest_not_mv" is not a materialized view
+DROP VIEW mvtest_not_mv;
+-- cannot use OR REPLACE with IF NOT EXISTS
+CREATE OR REPLACE MATERIALIZED VIEW IF NOT EXISTS mvtest_replace AS
+  SELECT 1 AS a;
+ERROR:  syntax error at or near "NOT"
+LINE 1: CREATE OR REPLACE MATERIALIZED VIEW IF NOT EXISTS mvtest_rep...
+                                               ^
+DROP MATERIALIZED VIEW mvtest_replace;
diff --git a/src/test/regress/sql/matview.sql b/src/test/regress/sql/matview.sql
index b74ee305e0..c12f0243c9 100644
--- a/src/test/regress/sql/matview.sql
+++ b/src/test/regress/sql/matview.sql
@@ -314,3 +314,111 @@ EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF)
   CREATE MATERIALIZED VIEW IF NOT EXISTS matview_ine_tab AS
     SELECT 1 / 0 WITH NO DATA; -- ok
 DROP MATERIALIZED VIEW matview_ine_tab;
+
+--
+-- test CREATE OR REPLACE MATERIALIZED VIEW
+--
+
+-- matview does not already exist
+DROP MATERIALIZED VIEW IF EXISTS mvtest_replace;
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 1 AS a;
+SELECT * FROM mvtest_replace;
+
+-- replace query with data
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 2 AS a;
+SELECT * FROM mvtest_replace;
+
+-- replace query without data
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 3 AS a
+  WITH NO DATA;
+SELECT * FROM mvtest_replace; -- error: not populated
+REFRESH MATERIALIZED VIEW mvtest_replace;
+SELECT * FROM mvtest_replace;
+
+-- add column
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 4 AS a, 1 b;
+SELECT * FROM mvtest_replace;
+
+-- replace table options
+SELECT m.*, c.relname, c.reloptions, s.spcname, a.amname
+  FROM mvtest_replace m
+    CROSS JOIN pg_class c
+    LEFT JOIN pg_tablespace s ON s.oid = c.reltablespace
+    LEFT JOIN pg_am a ON a.oid = c.relam
+  WHERE c.relname = 'mvtest_replace';
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace
+  USING heap2
+  WITH (fillfactor = 50)
+  TABLESPACE regress_tblspace
+  AS SELECT 5 AS a, 1 AS b;
+SELECT m.*, c.relname, c.reloptions, s.spcname, a.amname
+  FROM mvtest_replace m
+    CROSS JOIN pg_class c
+    LEFT JOIN pg_tablespace s ON s.oid = c.reltablespace
+    LEFT JOIN pg_am a ON a.oid = c.relam
+  WHERE c.relname = 'mvtest_replace';
+
+-- can replace matview that has a dependent view
+CREATE VIEW mvtest_replace_v AS
+  SELECT * FROM mvtest_replace;
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 6 AS a, 1 AS b;
+SELECT * FROM mvtest_replace, mvtest_replace_v;
+DROP VIEW mvtest_replace_v;
+
+-- index gets rebuilt when replacing with data
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 7 AS a, 1 AS b;
+CREATE UNIQUE INDEX ON mvtest_replace (b);
+SELECT * FROM mvtest_replace;
+SET enable_seqscan = off; -- force index scan
+EXPLAIN (COSTS OFF) SELECT * FROM mvtest_replace WHERE b = 1;
+SELECT * FROM mvtest_replace WHERE b = 1;
+RESET enable_seqscan;
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 8 AS a, 1 AS b;
+SET enable_seqscan = off; -- force index scan
+EXPLAIN (COSTS OFF) SELECT * FROM mvtest_replace WHERE b = 1;
+SELECT * FROM mvtest_replace WHERE b = 1;
+RESET enable_seqscan;
+
+-- cannot change column data type
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 9 AS a, 'x' AS b; -- error
+SELECT * FROM mvtest_replace;
+
+-- cannot rename column
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 10 AS a, 1 AS b2; -- error
+SELECT * FROM mvtest_replace;
+
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 11 AS a, 1 AS b, 'y' COLLATE "C" AS c;
+SELECT * FROM mvtest_replace;
+
+-- cannot change column collation
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 12 AS a, 1 AS b, 'x' COLLATE "POSIX" AS c; -- error
+SELECT * FROM mvtest_replace;
+
+-- cannot drop column
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 13 AS a, 1 AS b; -- error
+SELECT * FROM mvtest_replace;
+
+-- must target a matview
+CREATE VIEW mvtest_not_mv AS
+  SELECT 1 AS a;
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_not_mv AS
+  SELECT 1 AS a; -- error
+DROP VIEW mvtest_not_mv;
+
+-- cannot use OR REPLACE with IF NOT EXISTS
+CREATE OR REPLACE MATERIALIZED VIEW IF NOT EXISTS mvtest_replace AS
+  SELECT 1 AS a;
+
+DROP MATERIALIZED VIEW mvtest_replace;
-- 
2.45.2

  [text/x-diff] v1-0002-Deprecate-CREATE-MATERIALIZED-VIEW-IF-NOT-EXISTS.patch (8.5K, ../../3c86a16f-4272-4df3-9959-70a9a7d88a71@ewie.name/3-v1-0002-Deprecate-CREATE-MATERIALIZED-VIEW-IF-NOT-EXISTS.patch)
  download | inline diff:
From 52b0bc5d02755a37c2c04040d1eb74b8f59f12ea Mon Sep 17 00:00:00 2001
From: Erik Wienhold <ewie@ewie.name>
Date: Tue, 28 May 2024 02:19:53 +0200
Subject: [PATCH v1 2/2] Deprecate CREATE MATERIALIZED VIEW IF NOT EXISTS

---
 src/backend/parser/gram.y                     | 14 +++++++++++++
 .../expected/test_extensions.out              | 18 +++++++++++++++++
 src/test/regress/expected/matview.out         | 20 +++++++++++++++++++
 3 files changed, 52 insertions(+)

diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index ed477806d1..b1100bdec1 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -4830,6 +4830,20 @@ CreateMatViewStmt:
 					$8->rel->relpersistence = $2;
 					$8->skipData = !($11);
 					$$ = (Node *) ctas;
+
+					if (ctas->into->rel->schemaname)
+							ereport(WARNING,
+									errmsg("IF NOT EXISTS is deprecated in materialized view creation"),
+									errhint("Use CREATE OR REPLACE MATERIALIZED VIEW %s.%s.",
+											ctas->into->rel->schemaname,
+											ctas->into->rel->relname),
+									parser_errposition(@1));
+					else
+							ereport(WARNING,
+									errmsg("IF NOT EXISTS is deprecated in materialized view creation"),
+									errhint("Use CREATE OR REPLACE MATERIALIZED VIEW %s.",
+											ctas->into->rel->relname),
+									parser_errposition(@1));
 				}
 		| CREATE OR REPLACE OptNoLog MATERIALIZED VIEW create_mv_target AS SelectStmt opt_with_data
 				{
diff --git a/src/test/modules/test_extensions/expected/test_extensions.out b/src/test/modules/test_extensions/expected/test_extensions.out
index f357cc21aa..ecd453b29d 100644
--- a/src/test/modules/test_extensions/expected/test_extensions.out
+++ b/src/test/modules/test_extensions/expected/test_extensions.out
@@ -377,41 +377,57 @@ Objects in extension "test_ext_cor"
 CREATE COLLATION ext_cine_coll
   ( LC_COLLATE = "C", LC_CTYPE = "C" );
 CREATE EXTENSION test_ext_cine;  -- fail
+WARNING:  IF NOT EXISTS is deprecated in materialized view creation
+HINT:  Use CREATE OR REPLACE MATERIALIZED VIEW ext_cine_mv.
 ERROR:  collation ext_cine_coll is not a member of extension "test_ext_cine"
 DETAIL:  An extension may only use CREATE ... IF NOT EXISTS to skip object creation if the conflicting object is one that it already owns.
 DROP COLLATION ext_cine_coll;
 CREATE MATERIALIZED VIEW ext_cine_mv AS SELECT 11 AS f1;
 CREATE EXTENSION test_ext_cine;  -- fail
+WARNING:  IF NOT EXISTS is deprecated in materialized view creation
+HINT:  Use CREATE OR REPLACE MATERIALIZED VIEW ext_cine_mv.
 ERROR:  materialized view ext_cine_mv is not a member of extension "test_ext_cine"
 DETAIL:  An extension may only use CREATE ... IF NOT EXISTS to skip object creation if the conflicting object is one that it already owns.
 DROP MATERIALIZED VIEW ext_cine_mv;
 CREATE FOREIGN DATA WRAPPER dummy;
 CREATE SERVER ext_cine_srv FOREIGN DATA WRAPPER dummy;
 CREATE EXTENSION test_ext_cine;  -- fail
+WARNING:  IF NOT EXISTS is deprecated in materialized view creation
+HINT:  Use CREATE OR REPLACE MATERIALIZED VIEW ext_cine_mv.
 ERROR:  server ext_cine_srv is not a member of extension "test_ext_cine"
 DETAIL:  An extension may only use CREATE ... IF NOT EXISTS to skip object creation if the conflicting object is one that it already owns.
 DROP SERVER ext_cine_srv;
 CREATE SCHEMA ext_cine_schema;
 CREATE EXTENSION test_ext_cine;  -- fail
+WARNING:  IF NOT EXISTS is deprecated in materialized view creation
+HINT:  Use CREATE OR REPLACE MATERIALIZED VIEW ext_cine_mv.
 ERROR:  schema ext_cine_schema is not a member of extension "test_ext_cine"
 DETAIL:  An extension may only use CREATE ... IF NOT EXISTS to skip object creation if the conflicting object is one that it already owns.
 DROP SCHEMA ext_cine_schema;
 CREATE SEQUENCE ext_cine_seq;
 CREATE EXTENSION test_ext_cine;  -- fail
+WARNING:  IF NOT EXISTS is deprecated in materialized view creation
+HINT:  Use CREATE OR REPLACE MATERIALIZED VIEW ext_cine_mv.
 ERROR:  sequence ext_cine_seq is not a member of extension "test_ext_cine"
 DETAIL:  An extension may only use CREATE ... IF NOT EXISTS to skip object creation if the conflicting object is one that it already owns.
 DROP SEQUENCE ext_cine_seq;
 CREATE TABLE ext_cine_tab1 (x int);
 CREATE EXTENSION test_ext_cine;  -- fail
+WARNING:  IF NOT EXISTS is deprecated in materialized view creation
+HINT:  Use CREATE OR REPLACE MATERIALIZED VIEW ext_cine_mv.
 ERROR:  table ext_cine_tab1 is not a member of extension "test_ext_cine"
 DETAIL:  An extension may only use CREATE ... IF NOT EXISTS to skip object creation if the conflicting object is one that it already owns.
 DROP TABLE ext_cine_tab1;
 CREATE TABLE ext_cine_tab2 AS SELECT 42 AS y;
 CREATE EXTENSION test_ext_cine;  -- fail
+WARNING:  IF NOT EXISTS is deprecated in materialized view creation
+HINT:  Use CREATE OR REPLACE MATERIALIZED VIEW ext_cine_mv.
 ERROR:  table ext_cine_tab2 is not a member of extension "test_ext_cine"
 DETAIL:  An extension may only use CREATE ... IF NOT EXISTS to skip object creation if the conflicting object is one that it already owns.
 DROP TABLE ext_cine_tab2;
 CREATE EXTENSION test_ext_cine;
+WARNING:  IF NOT EXISTS is deprecated in materialized view creation
+HINT:  Use CREATE OR REPLACE MATERIALIZED VIEW ext_cine_mv.
 \dx+ test_ext_cine
 Objects in extension "test_ext_cine"
         Object description         
@@ -433,6 +449,8 @@ Objects in extension "test_ext_cine"
 (14 rows)
 
 ALTER EXTENSION test_ext_cine UPDATE TO '1.1';
+WARNING:  IF NOT EXISTS is deprecated in materialized view creation
+HINT:  Use CREATE OR REPLACE MATERIALIZED VIEW ext_cine_mv.
 \dx+ test_ext_cine
 Objects in extension "test_ext_cine"
         Object description         
diff --git a/src/test/regress/expected/matview.out b/src/test/regress/expected/matview.out
index e2e2a13396..cefd0d442c 100644
--- a/src/test/regress/expected/matview.out
+++ b/src/test/regress/expected/matview.out
@@ -565,6 +565,10 @@ CREATE MATERIALIZED VIEW mvtest_mv_foo AS SELECT * FROM mvtest_foo_data;
 CREATE MATERIALIZED VIEW mvtest_mv_foo AS SELECT * FROM mvtest_foo_data;
 ERROR:  relation "mvtest_mv_foo" already exists
 CREATE MATERIALIZED VIEW IF NOT EXISTS mvtest_mv_foo AS SELECT * FROM mvtest_foo_data;
+WARNING:  IF NOT EXISTS is deprecated in materialized view creation
+LINE 1: CREATE MATERIALIZED VIEW IF NOT EXISTS mvtest_mv_foo AS SELE...
+        ^
+HINT:  Use CREATE OR REPLACE MATERIALIZED VIEW mvtest_mv_foo.
 NOTICE:  relation "mvtest_mv_foo" already exists, skipping
 CREATE UNIQUE INDEX ON mvtest_mv_foo (i);
 RESET ROLE;
@@ -662,12 +666,20 @@ CREATE MATERIALIZED VIEW matview_ine_tab AS SELECT 1 / 0; -- error
 ERROR:  relation "matview_ine_tab" already exists
 CREATE MATERIALIZED VIEW IF NOT EXISTS matview_ine_tab AS
   SELECT 1 / 0; -- ok
+WARNING:  IF NOT EXISTS is deprecated in materialized view creation
+LINE 1: CREATE MATERIALIZED VIEW IF NOT EXISTS matview_ine_tab AS
+        ^
+HINT:  Use CREATE OR REPLACE MATERIALIZED VIEW matview_ine_tab.
 NOTICE:  relation "matview_ine_tab" already exists, skipping
 CREATE MATERIALIZED VIEW matview_ine_tab AS
   SELECT 1 / 0 WITH NO DATA; -- error
 ERROR:  relation "matview_ine_tab" already exists
 CREATE MATERIALIZED VIEW IF NOT EXISTS matview_ine_tab AS
   SELECT 1 / 0 WITH NO DATA; -- ok
+WARNING:  IF NOT EXISTS is deprecated in materialized view creation
+LINE 1: CREATE MATERIALIZED VIEW IF NOT EXISTS matview_ine_tab AS
+        ^
+HINT:  Use CREATE OR REPLACE MATERIALIZED VIEW matview_ine_tab.
 NOTICE:  relation "matview_ine_tab" already exists, skipping
 EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF)
   CREATE MATERIALIZED VIEW matview_ine_tab AS
@@ -676,6 +688,10 @@ ERROR:  relation "matview_ine_tab" already exists
 EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF)
   CREATE MATERIALIZED VIEW IF NOT EXISTS matview_ine_tab AS
     SELECT 1 / 0; -- ok
+WARNING:  IF NOT EXISTS is deprecated in materialized view creation
+LINE 2:   CREATE MATERIALIZED VIEW IF NOT EXISTS matview_ine_tab AS
+          ^
+HINT:  Use CREATE OR REPLACE MATERIALIZED VIEW matview_ine_tab.
 NOTICE:  relation "matview_ine_tab" already exists, skipping
  QUERY PLAN 
 ------------
@@ -688,6 +704,10 @@ ERROR:  relation "matview_ine_tab" already exists
 EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF)
   CREATE MATERIALIZED VIEW IF NOT EXISTS matview_ine_tab AS
     SELECT 1 / 0 WITH NO DATA; -- ok
+WARNING:  IF NOT EXISTS is deprecated in materialized view creation
+LINE 2:   CREATE MATERIALIZED VIEW IF NOT EXISTS matview_ine_tab AS
+          ^
+HINT:  Use CREATE OR REPLACE MATERIALIZED VIEW matview_ine_tab.
 NOTICE:  relation "matview_ine_tab" already exists, skipping
  QUERY PLAN 
 ------------
-- 
2.45.2

^ permalink  raw  reply  [nested|flat] 29+ messages in thread

* Re: CREATE OR REPLACE MATERIALIZED VIEW
@ 2024-07-02 10:46  Aleksander Alekseev <aleksander@timescale.com>
  parent: Erik Wienhold <ewie@ewie.name>
  2 siblings, 1 reply; 29+ messages in thread

From: Aleksander Alekseev @ 2024-07-02 10:46 UTC (permalink / raw)
  To: pgsql-hackers; +Cc: Erik Wienhold <ewie@ewie.name>

Hi,

> Patch 0002 deprecates CREATE MATERIALIZED VIEW IF NOT EXISTS because it
> no longer seems necessary with patch 0001.  Tom Lane commented[1] about
> the general dislike of IF NOT EXISTS, to which I agree, but maybe this
> was meant only in response to adding new commands.  Anyway, my idea is
> to deprecate that usage in PG18 and eventually remove it in PG19, if
> there's consensus for it.  We can drop that clause without violating any
> standard because matviews are a Postgres extension.  I'm not married to
> the idea, just want to put it on the table for discussion.

I can imagine how this may impact many applications and upset many
software developers worldwide. Was there even a precedent (in the
recent decade or so) when PostgreSQL broke the SQL syntax?

To clarify, I'm not opposed to this idea. If we are fine with breaking
backward compatibility on the SQL level, this would allow dropping the
support of inherited tables some day, a feature that in my humble
opinion shouldn't exist (I realize this is another and very debatable
question though). I just don't think this is something we ever do in
this project. But I admit that this information may be incorrect
and/or outdated.

-- 
Best regards,
Aleksander Alekseev





^ permalink  raw  reply  [nested|flat] 29+ messages in thread

* Re: CREATE OR REPLACE MATERIALIZED VIEW
@ 2024-07-02 12:27  Daniel Gustafsson <daniel@yesql.se>
  parent: Erik Wienhold <ewie@ewie.name>
  2 siblings, 1 reply; 29+ messages in thread

From: Daniel Gustafsson @ 2024-07-02 12:27 UTC (permalink / raw)
  To: Erik Wienhold <ewie@ewie.name>; +Cc: pgsql-hackers

> On 2 Jul 2024, at 03:22, Erik Wienhold <ewie@ewie.name> wrote:

> Patch 0002 deprecates CREATE MATERIALIZED VIEW IF NOT EXISTS because it
> no longer seems necessary with patch 0001.  Tom Lane commented[1] about
> the general dislike of IF NOT EXISTS, to which I agree, but maybe this
> was meant only in response to adding new commands.  Anyway, my idea is
> to deprecate that usage in PG18 and eventually remove it in PG19, if
> there's consensus for it.

Considering the runway we typically give for deprecations, that seems like a
fairly short timeframe for a SQL level command which isn't unlikely to exist
in application code.

--
Daniel Gustafsson






^ permalink  raw  reply  [nested|flat] 29+ messages in thread

* Re: CREATE OR REPLACE MATERIALIZED VIEW
@ 2024-07-02 13:58  Erik Wienhold <ewie@ewie.name>
  parent: Daniel Gustafsson <daniel@yesql.se>
  0 siblings, 1 reply; 29+ messages in thread

From: Erik Wienhold @ 2024-07-02 13:58 UTC (permalink / raw)
  To: Daniel Gustafsson <daniel@yesql.se>; +Cc: pgsql-hackers; Aleksander Alekseev <aleksander@timescale.com>

I wrote:
> Patch 0002 deprecates CREATE MATERIALIZED VIEW IF NOT EXISTS because it
> no longer seems necessary with patch 0001.  Tom Lane commented[1] about
> the general dislike of IF NOT EXISTS, to which I agree, but maybe this
> was meant only in response to adding new commands.

One could also argue that since matviews are a hybrid of tables and
views, that CREATE MATERIALIZED VIEW should accept both OR REPLACE (as
in CREATE VIEW) and IF NOT EXISTS (as in CREATE TABLE).  But not in the
same invocation of course.

On 2024-07-02 12:46 +0200, Aleksander Alekseev wrote:
> > Anyway, my idea is to deprecate that usage in PG18 and eventually
> > remove it in PG19, if there's consensus for it.  We can drop that
> > clause without violating any standard because matviews are a
> > Postgres extension.  I'm not married to the idea, just want to put
> > it on the table for discussion.
> 
> I can imagine how this may impact many applications and upset many
> software developers worldwide. Was there even a precedent (in the
> recent decade or so) when PostgreSQL broke the SQL syntax?

A quick spelunking through the changelog with

    git log --grep deprecat -i --since '10 years ago'

turned up two commits:

578b229718 "Remove WITH OIDS support, change oid catalog column visibility."
e8d016d819 "Remove deprecated COMMENT ON RULE syntax"

Both were committed more than 10 years after deprecating the respective
feature.  My proposed one-year window seems a bit harsh in comparison.

On 2024-07-02 14:27 +0200, Daniel Gustafsson wrote:
> Considering the runway we typically give for deprecations, that seems like a
> fairly short timeframe for a SQL level command which isn't unlikely to exist
> in application code.

Is there some general agreed upon timeframe, or is decided on a
case-by-case basis?  I can imagine waiting at least until the last
release without the deprecation reaches EOL.  That would be 5 years with
the current versioning policy.

-- 
Erik





^ permalink  raw  reply  [nested|flat] 29+ messages in thread

* Re: CREATE OR REPLACE MATERIALIZED VIEW
@ 2024-07-02 14:01  Daniel Gustafsson <daniel@yesql.se>
  parent: Erik Wienhold <ewie@ewie.name>
  0 siblings, 0 replies; 29+ messages in thread

From: Daniel Gustafsson @ 2024-07-02 14:01 UTC (permalink / raw)
  To: Erik Wienhold <ewie@ewie.name>; +Cc: pgsql-hackers; Aleksander Alekseev <aleksander@timescale.com>

> On 2 Jul 2024, at 15:58, Erik Wienhold <ewie@ewie.name> wrote:
> On 2024-07-02 14:27 +0200, Daniel Gustafsson wrote:
>> Considering the runway we typically give for deprecations, that seems like a
>> fairly short timeframe for a SQL level command which isn't unlikely to exist
>> in application code.
> 
> Is there some general agreed upon timeframe, or is decided on a
> case-by-case basis?  I can imagine waiting at least until the last
> release without the deprecation reaches EOL.  That would be 5 years with
> the current versioning policy.

AFAIK it's all decided on a case-by-case basis depending on impact.  There are
for example the removals you listed, and there are functions in libpq which
were deprecated in the postgres 6.x days which are still around to avoid
breaking ABI.

--
Daniel Gustafsson






^ permalink  raw  reply  [nested|flat] 29+ messages in thread

* Re: CREATE OR REPLACE MATERIALIZED VIEW
@ 2024-07-04 20:18  Said Assemlal <sassemlal@neurorx.com>
  parent: Erik Wienhold <ewie@ewie.name>
  2 siblings, 1 reply; 29+ messages in thread

From: Said Assemlal @ 2024-07-04 20:18 UTC (permalink / raw)
  To: Erik Wienhold <ewie@ewie.name>; pgsql-hackers

Hi,


+1 for this feature.

> Replacing Matviews
> ------------------
>
> With patch 0001, a matview can be replaced without having to drop it and
> its dependent objects.  In our use case it is no longer necessary to
> define the actual query in a separate view.  Replacing a matview works
> analogous to CREATE OR REPLACE VIEW:
>
> * the new query may change SELECT list expressions of existing columns
> * new columns can be added to the end of the SELECT list
> * existing columns cannot be renamed
> * the data type of existing columns cannot be changed
>
> In addition to that, CREATE OR REPLACE MATERIALIZED VIEW also replaces
> access method, tablespace, and storage parameters if specified.  The
> clause WITH [NO] DATA works as expected: it either populates the matview
> or leaves it in an unscannable state.
>
> It is an error to specify both OR REPLACE and IF NOT EXISTS.

I noticed replacing the materialized view is blocking all reads. Is that 
expected ? Even if there is a unique index ?


Best,
Sa_ïd_

^ permalink  raw  reply  [nested|flat] 29+ messages in thread

* Re: CREATE OR REPLACE MATERIALIZED VIEW
@ 2024-07-05 23:42  Erik Wienhold <ewie@ewie.name>
  parent: Said Assemlal <sassemlal@neurorx.com>
  0 siblings, 1 reply; 29+ messages in thread

From: Erik Wienhold @ 2024-07-05 23:42 UTC (permalink / raw)
  To: Said Assemlal <sassemlal@neurorx.com>; +Cc: pgsql-hackers

On 2024-07-04 22:18 +0200, Said Assemlal wrote:
> +1 for this feature.

Thanks!

> I noticed replacing the materialized view is blocking all reads. Is that
> expected ? Even if there is a unique index ?

That is expected because AccessExclusiveLock is acquired on the existing
matview.  This is also the case for CREATE OR REPLACE VIEW.

My initial idea, while writing the patch, was that one could replace the
matview without populating it and then run the concurrent refresh, like
this:

    CREATE OR REPLACE MATERIALIZED VIEW foo AS ... WITH NO DATA;
    REFRESH MATERIALIZED VIEW CONCURRENTLY foo;

But that won't work because concurrent refresh requires an already
populated matview.

Right now the patch either populates the replaced matview or leaves it
in an unscannable state.  Technically, it's also possible to skip the
refresh and leave the old data in place, perhaps by specifying
WITH *OLD* DATA.  New columns would just be null.  Of course you can't
tell if you got stale data without knowing how the matview was replaced.
Thoughts?

-- 
Erik





^ permalink  raw  reply  [nested|flat] 29+ messages in thread

* Re: CREATE OR REPLACE MATERIALIZED VIEW
@ 2024-07-12 14:49  Said Assemlal <sassemlal@neurorx.com>
  parent: Erik Wienhold <ewie@ewie.name>
  0 siblings, 1 reply; 29+ messages in thread

From: Said Assemlal @ 2024-07-12 14:49 UTC (permalink / raw)
  To: Erik Wienhold <ewie@ewie.name>; +Cc: pgsql-hackers


> That is expected because AccessExclusiveLock is acquired on the existing
> matview.  This is also the case for CREATE OR REPLACE VIEW.

Right, had this case many times.

>
> My initial idea, while writing the patch, was that one could replace the
> matview without populating it and then run the concurrent refresh, like
> this:
>
>      CREATE OR REPLACE MATERIALIZED VIEW foo AS ... WITH NO DATA;
>      REFRESH MATERIALIZED VIEW CONCURRENTLY foo;
>
> But that won't work because concurrent refresh requires an already
> populated matview.
>
> Right now the patch either populates the replaced matview or leaves it
> in an unscannable state.  Technically, it's also possible to skip the
> refresh and leave the old data in place, perhaps by specifying
> WITH *OLD* DATA.  New columns would just be null.  Of course you can't
> tell if you got stale data without knowing how the matview was replaced.
> Thoughts?


I believe the expectation is to get materialized views updated whenever 
it gets replaced so likely to confuse users ?









^ permalink  raw  reply  [nested|flat] 29+ messages in thread

* Re: CREATE OR REPLACE MATERIALIZED VIEW
@ 2024-07-27 00:45  Erik Wienhold <ewie@ewie.name>
  parent: Said Assemlal <sassemlal@neurorx.com>
  0 siblings, 1 reply; 29+ messages in thread

From: Erik Wienhold @ 2024-07-27 00:45 UTC (permalink / raw)
  To: Said Assemlal <sassemlal@neurorx.com>; +Cc: pgsql-hackers

On 2024-07-12 16:49 +0200, Said Assemlal wrote:
> > My initial idea, while writing the patch, was that one could replace the
> > matview without populating it and then run the concurrent refresh, like
> > this:
> > 
> >      CREATE OR REPLACE MATERIALIZED VIEW foo AS ... WITH NO DATA;
> >      REFRESH MATERIALIZED VIEW CONCURRENTLY foo;
> > 
> > But that won't work because concurrent refresh requires an already
> > populated matview.
> > 
> > Right now the patch either populates the replaced matview or leaves it
> > in an unscannable state.  Technically, it's also possible to skip the
> > refresh and leave the old data in place, perhaps by specifying
> > WITH *OLD* DATA.  New columns would just be null.  Of course you can't
> > tell if you got stale data without knowing how the matview was replaced.
> > Thoughts?
> 
> I believe the expectation is to get materialized views updated whenever it
> gets replaced so likely to confuse users ?

I agree, that could be confusing -- unless it's well documented.  The
attached 0003 implements WITH OLD DATA and states in the docs that this
is intended to be used before a concurrent refresh.

Patch 0001 now covers all matview cases in psql's tab completion.  I
missed some of them with v1.

-- 
Erik

Attachments:

  [text/x-diff] v2-0001-Add-CREATE-OR-REPLACE-MATERIALIZED-VIEW.patch (31.8K, ../../7afe68b0-f983-4a9f-a1b4-32188cebbf38@ewie.name/2-v2-0001-Add-CREATE-OR-REPLACE-MATERIALIZED-VIEW.patch)
  download | inline diff:
From a529a00af40be611e6bed49fe0341b7435a72930 Mon Sep 17 00:00:00 2001
From: Erik Wienhold <ewie@ewie.name>
Date: Tue, 21 May 2024 18:35:47 +0200
Subject: [PATCH v2 1/3] Add CREATE OR REPLACE MATERIALIZED VIEW

---
 .../sgml/ref/create_materialized_view.sgml    |  15 +-
 src/backend/commands/createas.c               | 207 ++++++++++++++----
 src/backend/commands/tablecmds.c              |   8 +-
 src/backend/commands/view.c                   | 106 ++++++---
 src/backend/parser/gram.y                     |  15 ++
 src/bin/psql/tab-complete.c                   |  15 +-
 src/include/commands/view.h                   |   3 +
 src/include/nodes/parsenodes.h                |   2 +-
 src/include/nodes/primnodes.h                 |   1 +
 src/test/regress/expected/matview.out         | 191 ++++++++++++++++
 src/test/regress/sql/matview.sql              | 108 +++++++++
 11 files changed, 582 insertions(+), 89 deletions(-)

diff --git a/doc/src/sgml/ref/create_materialized_view.sgml b/doc/src/sgml/ref/create_materialized_view.sgml
index 0d2fea2b97..b5a8e3441a 100644
--- a/doc/src/sgml/ref/create_materialized_view.sgml
+++ b/doc/src/sgml/ref/create_materialized_view.sgml
@@ -21,7 +21,7 @@ PostgreSQL documentation
 
  <refsynopsisdiv>
 <synopsis>
-CREATE MATERIALIZED VIEW [ IF NOT EXISTS ] <replaceable>table_name</replaceable>
+CREATE [ OR REPLACE ] MATERIALIZED VIEW [ IF NOT EXISTS ] <replaceable>table_name</replaceable>
     [ (<replaceable>column_name</replaceable> [, ...] ) ]
     [ USING <replaceable class="parameter">method</replaceable> ]
     [ WITH ( <replaceable class="parameter">storage_parameter</replaceable> [= <replaceable class="parameter">value</replaceable>] [, ... ] ) ]
@@ -60,6 +60,17 @@ CREATE MATERIALIZED VIEW [ IF NOT EXISTS ] <replaceable>table_name</replaceable>
   <title>Parameters</title>
 
   <variablelist>
+   <varlistentry>
+    <term><literal>OR REPLACE</literal></term>
+    <listitem>
+     <para>
+      Replaces a materialized view if it already exists.
+      Specifying <literal>OR REPLACE</literal> together with
+      <literal>IF NOT EXISTS</literal> is an error.
+     </para>
+    </listitem>
+   </varlistentry>
+
    <varlistentry>
     <term><literal>IF NOT EXISTS</literal></term>
     <listitem>
@@ -67,7 +78,7 @@ CREATE MATERIALIZED VIEW [ IF NOT EXISTS ] <replaceable>table_name</replaceable>
       Do not throw an error if a materialized view with the same name already
       exists. A notice is issued in this case.  Note that there is no guarantee
       that the existing materialized view is anything like the one that would
-      have been created.
+      have been created, unless you use <literal>OR REPLACE</literal> instead.
      </para>
     </listitem>
    </varlistentry>
diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c
index 2c8a93b6e5..c5d78252a1 100644
--- a/src/backend/commands/createas.c
+++ b/src/backend/commands/createas.c
@@ -79,55 +79,151 @@ static void intorel_destroy(DestReceiver *self);
 static ObjectAddress
 create_ctas_internal(List *attrList, IntoClause *into)
 {
-	CreateStmt *create = makeNode(CreateStmt);
-	bool		is_matview;
+	bool		is_matview,
+				replace = false;
 	char		relkind;
-	Datum		toast_options;
-	static char *validnsps[] = HEAP_RELOPT_NAMESPACES;
+	Oid			matviewOid = InvalidOid;
 	ObjectAddress intoRelationAddr;
 
 	/* This code supports both CREATE TABLE AS and CREATE MATERIALIZED VIEW */
 	is_matview = (into->viewQuery != NULL);
 	relkind = is_matview ? RELKIND_MATVIEW : RELKIND_RELATION;
 
-	/*
-	 * Create the target relation by faking up a CREATE TABLE parsetree and
-	 * passing it to DefineRelation.
-	 */
-	create->relation = into->rel;
-	create->tableElts = attrList;
-	create->inhRelations = NIL;
-	create->ofTypename = NULL;
-	create->constraints = NIL;
-	create->options = into->options;
-	create->oncommit = into->onCommit;
-	create->tablespacename = into->tableSpaceName;
-	create->if_not_exists = false;
-	create->accessMethod = into->accessMethod;
+	/* Check if an existing materialized view needs to be replaced. */
+	if (is_matview)
+	{
+		LOCKMODE	lockmode;
 
-	/*
-	 * Create the relation.  (This will error out if there's an existing view,
-	 * so we don't need more code to complain if "replace" is false.)
-	 */
-	intoRelationAddr = DefineRelation(create, relkind, InvalidOid, NULL, NULL);
+		lockmode = into->replace ? AccessExclusiveLock : NoLock;
+		(void) RangeVarGetAndCheckCreationNamespace(into->rel, lockmode,
+													&matviewOid);
+		replace = OidIsValid(matviewOid) && into->replace;
+	}
 
-	/*
-	 * If necessary, create a TOAST table for the target table.  Note that
-	 * NewRelationCreateToastTable ends with CommandCounterIncrement(), so
-	 * that the TOAST table will be visible for insertion.
-	 */
-	CommandCounterIncrement();
+	if (is_matview && replace)
+	{
+		Relation	rel;
+		List	   *atcmds = NIL;
+		AlterTableCmd *atcmd;
+		TupleDesc	descriptor;
+
+		rel = relation_open(matviewOid, NoLock);
+
+		if (rel->rd_rel->relkind != RELKIND_MATVIEW)
+			ereport(ERROR,
+					errcode(ERRCODE_WRONG_OBJECT_TYPE),
+					errmsg("\"%s\" is not a materialized view",
+						   RelationGetRelationName(rel)));
+
+		CheckTableNotInUse(rel, "CREATE OR REPLACE MATERIALIZED VIEW");
+
+		descriptor = BuildDescForRelation(attrList);
+		checkViewColumns(descriptor, rel->rd_att, true);
+
+		/* Add new attributes via ALTER TABLE. */
+		if (list_length(attrList) > rel->rd_att->natts)
+		{
+			ListCell   *c;
+			int			skip = rel->rd_att->natts;
+
+			foreach(c, attrList)
+			{
+				if (skip > 0)
+				{
+					skip--;
+					continue;
+				}
+				atcmd = makeNode(AlterTableCmd);
+				atcmd->subtype = AT_AddColumnToView;
+				atcmd->def = (Node *) lfirst(c);
+				atcmds = lappend(atcmds, atcmd);
+			}
+		}
+
+		/* Set access method via ALTER TABLE. */
+		if (into->accessMethod != NULL)
+		{
+			atcmd = makeNode(AlterTableCmd);
+			atcmd->subtype = AT_SetAccessMethod;
+			atcmd->name = into->accessMethod;
+			atcmds = lappend(atcmds, atcmd);
+		}
+
+		/* Set tablespace via ALTER TABLE. */
+		if (into->tableSpaceName != NULL)
+		{
+			atcmd = makeNode(AlterTableCmd);
+			atcmd->subtype = AT_SetTableSpace;
+			atcmd->name = into->tableSpaceName;
+			atcmds = lappend(atcmds, atcmd);
+		}
+
+		/* Set storage parameters via ALTER TABLE. */
+		if (into->options != NIL)
+		{
+			atcmd = makeNode(AlterTableCmd);
+			atcmd->subtype = AT_ReplaceRelOptions;
+			atcmd->def = (Node *) into->options;
+			atcmds = lappend(atcmds, atcmd);
+		}
+
+		if (atcmds != NIL)
+		{
+			AlterTableInternal(matviewOid, atcmds, true);
+			CommandCounterIncrement();
+		}
+
+		relation_close(rel, NoLock);
+
+		ObjectAddressSet(intoRelationAddr, RelationRelationId, matviewOid);
+	}
+	else
+	{
+		CreateStmt *create = makeNode(CreateStmt);
+		Datum		toast_options;
+		static char *validnsps[] = HEAP_RELOPT_NAMESPACES;
+
+		/*
+		 * Create the target relation by faking up a CREATE TABLE parsetree
+		 * and passing it to DefineRelation.
+		 */
+		create->relation = into->rel;
+		create->tableElts = attrList;
+		create->inhRelations = NIL;
+		create->ofTypename = NULL;
+		create->constraints = NIL;
+		create->options = into->options;
+		create->oncommit = into->onCommit;
+		create->tablespacename = into->tableSpaceName;
+		create->if_not_exists = false;
+		create->accessMethod = into->accessMethod;
+
+		/*
+		 * Create the relation.  (This will error out if there's an existing
+		 * view, so we don't need more code to complain if "replace" is
+		 * false.)
+		 */
+		intoRelationAddr = DefineRelation(create, relkind, InvalidOid, NULL,
+										  NULL);
 
-	/* parse and validate reloptions for the toast table */
-	toast_options = transformRelOptions((Datum) 0,
-										create->options,
-										"toast",
-										validnsps,
-										true, false);
+		/*
+		 * If necessary, create a TOAST table for the target table.  Note that
+		 * NewRelationCreateToastTable ends with CommandCounterIncrement(), so
+		 * that the TOAST table will be visible for insertion.
+		 */
+		CommandCounterIncrement();
+
+		/* parse and validate reloptions for the toast table */
+		toast_options = transformRelOptions((Datum) 0,
+											create->options,
+											"toast",
+											validnsps,
+											true, false);
 
-	(void) heap_reloptions(RELKIND_TOASTVALUE, toast_options, true);
+		(void) heap_reloptions(RELKIND_TOASTVALUE, toast_options, true);
 
-	NewRelationCreateToastTable(intoRelationAddr.objectId, toast_options);
+		NewRelationCreateToastTable(intoRelationAddr.objectId, toast_options);
+	}
 
 	/* Create the "view" part of a materialized view. */
 	if (is_matview)
@@ -135,7 +231,7 @@ create_ctas_internal(List *attrList, IntoClause *into)
 		/* StoreViewQuery scribbles on tree, so make a copy */
 		Query	   *query = (Query *) copyObject(into->viewQuery);
 
-		StoreViewQuery(intoRelationAddr.objectId, query, false);
+		StoreViewQuery(intoRelationAddr.objectId, query, replace);
 		CommandCounterIncrement();
 	}
 
@@ -234,7 +330,26 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt,
 
 	/* Check if the relation exists or not */
 	if (CreateTableAsRelExists(stmt))
+	{
+		/* An existing materialized view can be replaced. */
+		if (is_matview && into->replace)
+		{
+			RefreshMatViewStmt *refresh;
+
+			/* Change the relation to match the new query and other options. */
+			(void) create_ctas_nodata(query->targetList, into);
+
+			/* Refresh the materialized view with a fake statement. */
+			refresh = makeNode(RefreshMatViewStmt);
+			refresh->relation = into->rel;
+			refresh->skipData = into->skipData;
+			refresh->concurrent = false;
+
+			return ExecRefreshMatView(refresh, NULL, NULL, NULL);
+		}
+
 		return InvalidObjectAddress;
+	}
 
 	/*
 	 * Create the tuple receiver object and insert info it will need
@@ -395,14 +510,15 @@ CreateTableAsRelExists(CreateTableAsStmt *ctas)
 	oldrelid = get_relname_relid(into->rel->relname, nspid);
 	if (OidIsValid(oldrelid))
 	{
-		if (!ctas->if_not_exists)
+		if (!ctas->if_not_exists && !into->replace)
 			ereport(ERROR,
 					(errcode(ERRCODE_DUPLICATE_TABLE),
 					 errmsg("relation \"%s\" already exists",
 							into->rel->relname)));
 
 		/*
-		 * The relation exists and IF NOT EXISTS has been specified.
+		 * The relation exists and IF NOT EXISTS or OR REPLACE has been
+		 * specified.
 		 *
 		 * If we are in an extension script, insist that the pre-existing
 		 * object be a member of the extension, to avoid security risks.
@@ -410,11 +526,12 @@ CreateTableAsRelExists(CreateTableAsStmt *ctas)
 		ObjectAddressSet(address, RelationRelationId, oldrelid);
 		checkMembershipInCurrentExtension(&address);
 
-		/* OK to skip */
-		ereport(NOTICE,
-				(errcode(ERRCODE_DUPLICATE_TABLE),
-				 errmsg("relation \"%s\" already exists, skipping",
-						into->rel->relname)));
+		if (ctas->if_not_exists)
+			/* OK to skip */
+			ereport(NOTICE,
+					(errcode(ERRCODE_DUPLICATE_TABLE),
+					 errmsg("relation \"%s\" already exists, skipping",
+							into->rel->relname)));
 		return true;
 	}
 
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 0b2a52463f..5274daaf3a 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -4484,7 +4484,7 @@ AlterTableGetLockLevel(List *cmds)
 				 * Subcommands that may be visible to concurrent SELECTs
 				 */
 			case AT_DropColumn: /* change visible to SELECT */
-			case AT_AddColumnToView:	/* CREATE VIEW */
+			case AT_AddColumnToView:	/* via CREATE OR REPLACE [MATERIALIZED] VIEW */
 			case AT_DropOids:	/* used to equiv to DropColumn */
 			case AT_EnableAlwaysRule:	/* may change SELECT rules */
 			case AT_EnableReplicaRule:	/* may change SELECT rules */
@@ -4795,8 +4795,8 @@ ATPrepCmd(List **wqueue, Relation rel, AlterTableCmd *cmd,
 			/* Recursion occurs during execution phase */
 			pass = AT_PASS_ADD_COL;
 			break;
-		case AT_AddColumnToView:	/* add column via CREATE OR REPLACE VIEW */
-			ATSimplePermissions(cmd->subtype, rel, ATT_VIEW);
+		case AT_AddColumnToView:	/* via CREATE OR REPLACE [MATERIALIZED] VIEW */
+			ATSimplePermissions(cmd->subtype, rel, ATT_VIEW | ATT_MATVIEW);
 			ATPrepAddColumn(wqueue, rel, recurse, recursing, true, cmd,
 							lockmode, context);
 			/* Recursion occurs during execution phase */
@@ -5220,7 +5220,7 @@ ATExecCmd(List **wqueue, AlteredTableInfo *tab,
 	switch (cmd->subtype)
 	{
 		case AT_AddColumn:		/* ADD COLUMN */
-		case AT_AddColumnToView:	/* add column via CREATE OR REPLACE VIEW */
+		case AT_AddColumnToView:	/* via CREATE OR REPLACE [MATERIALIZED] VIEW */
 			address = ATExecAddColumn(wqueue, tab, rel, &cmd,
 									  cmd->recurse, false,
 									  lockmode, cur_pass, context);
diff --git a/src/backend/commands/view.c b/src/backend/commands/view.c
index fdad833832..76532aa35d 100644
--- a/src/backend/commands/view.c
+++ b/src/backend/commands/view.c
@@ -30,8 +30,6 @@
 #include "utils/lsyscache.h"
 #include "utils/rel.h"
 
-static void checkViewColumns(TupleDesc newdesc, TupleDesc olddesc);
-
 /*---------------------------------------------------------------------
  * DefineVirtualRelation
  *
@@ -130,7 +128,7 @@ DefineVirtualRelation(RangeVar *relation, List *tlist, bool replace,
 		 * column list.
 		 */
 		descriptor = BuildDescForRelation(attrList);
-		checkViewColumns(descriptor, rel->rd_att);
+		checkViewColumns(descriptor, rel->rd_att, false);
 
 		/*
 		 * If new attributes have been added, we must add pg_attribute entries
@@ -263,15 +261,22 @@ DefineVirtualRelation(RangeVar *relation, List *tlist, bool replace,
  * added to generate specific complaints.  Also, we allow the new view to have
  * more columns than the old.
  */
-static void
-checkViewColumns(TupleDesc newdesc, TupleDesc olddesc)
+void
+checkViewColumns(TupleDesc newdesc, TupleDesc olddesc, bool is_matview)
 {
 	int			i;
 
 	if (newdesc->natts < olddesc->natts)
-		ereport(ERROR,
-				(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
-				 errmsg("cannot drop columns from view")));
+	{
+		if (is_matview)
+			ereport(ERROR,
+					errcode(ERRCODE_INVALID_TABLE_DEFINITION),
+					errmsg("cannot drop columns from materialized view"));
+		else
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
+					 errmsg("cannot drop columns from view")));
+	}
 
 	for (i = 0; i < olddesc->natts; i++)
 	{
@@ -280,17 +285,34 @@ checkViewColumns(TupleDesc newdesc, TupleDesc olddesc)
 
 		/* XXX msg not right, but we don't support DROP COL on view anyway */
 		if (newattr->attisdropped != oldattr->attisdropped)
-			ereport(ERROR,
-					(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
-					 errmsg("cannot drop columns from view")));
+		{
+			if (is_matview)
+				ereport(ERROR,
+						errcode(ERRCODE_INVALID_TABLE_DEFINITION),
+						errmsg("cannot drop columns from materialized view"));
+			else
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
+						 errmsg("cannot drop columns from view")));
+		}
 
 		if (strcmp(NameStr(newattr->attname), NameStr(oldattr->attname)) != 0)
-			ereport(ERROR,
-					(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
-					 errmsg("cannot change name of view column \"%s\" to \"%s\"",
-							NameStr(oldattr->attname),
-							NameStr(newattr->attname)),
-					 errhint("Use ALTER VIEW ... RENAME COLUMN ... to change name of view column instead.")));
+		{
+			if (is_matview)
+				ereport(ERROR,
+						errcode(ERRCODE_INVALID_TABLE_DEFINITION),
+						errmsg("cannot change name of materialized view column \"%s\" to \"%s\"",
+							   NameStr(oldattr->attname),
+							   NameStr(newattr->attname)),
+						errhint("Use ALTER MATERIALIZED VIEW ... RENAME COLUMN ... to change name of materialized view column instead."));
+			else
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
+						 errmsg("cannot change name of view column \"%s\" to \"%s\"",
+								NameStr(oldattr->attname),
+								NameStr(newattr->attname)),
+						 errhint("Use ALTER VIEW ... RENAME COLUMN ... to change name of view column instead.")));
+		}
 
 		/*
 		 * We cannot allow type, typmod, or collation to change, since these
@@ -299,26 +321,48 @@ checkViewColumns(TupleDesc newdesc, TupleDesc olddesc)
 		 */
 		if (newattr->atttypid != oldattr->atttypid ||
 			newattr->atttypmod != oldattr->atttypmod)
-			ereport(ERROR,
-					(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
-					 errmsg("cannot change data type of view column \"%s\" from %s to %s",
-							NameStr(oldattr->attname),
-							format_type_with_typemod(oldattr->atttypid,
-													 oldattr->atttypmod),
-							format_type_with_typemod(newattr->atttypid,
-													 newattr->atttypmod))));
+		{
+			if (is_matview)
+				ereport(ERROR,
+						errcode(ERRCODE_INVALID_TABLE_DEFINITION),
+						errmsg("cannot change data type of materialized view column \"%s\" from %s to %s",
+							   NameStr(oldattr->attname),
+							   format_type_with_typemod(oldattr->atttypid,
+														oldattr->atttypmod),
+							   format_type_with_typemod(newattr->atttypid,
+														newattr->atttypmod)));
+			else
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
+						 errmsg("cannot change data type of view column \"%s\" from %s to %s",
+								NameStr(oldattr->attname),
+								format_type_with_typemod(oldattr->atttypid,
+														 oldattr->atttypmod),
+								format_type_with_typemod(newattr->atttypid,
+														 newattr->atttypmod))));
+		}
 
 		/*
 		 * At this point, attcollations should be both valid or both invalid,
 		 * so applying get_collation_name unconditionally should be fine.
 		 */
 		if (newattr->attcollation != oldattr->attcollation)
-			ereport(ERROR,
-					(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
-					 errmsg("cannot change collation of view column \"%s\" from \"%s\" to \"%s\"",
-							NameStr(oldattr->attname),
-							get_collation_name(oldattr->attcollation),
-							get_collation_name(newattr->attcollation))));
+		{
+			if (is_matview)
+				ereport(ERROR,
+						errcode(ERRCODE_INVALID_TABLE_DEFINITION),
+						errmsg("cannot change collation of materialized view column \"%s\" from \"%s\" to \"%s\"",
+							   NameStr(oldattr->attname),
+							   get_collation_name(oldattr->attcollation),
+							   get_collation_name(newattr->attcollation)));
+			else
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
+						 errmsg("cannot change collation of view column \"%s\" from \"%s\" to \"%s\"",
+								NameStr(oldattr->attname),
+								get_collation_name(oldattr->attcollation),
+								get_collation_name(newattr->attcollation))));
+		}
 	}
 
 	/*
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index a043fd4c66..ed477806d1 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -4831,6 +4831,21 @@ CreateMatViewStmt:
 					$8->skipData = !($11);
 					$$ = (Node *) ctas;
 				}
+		| CREATE OR REPLACE OptNoLog MATERIALIZED VIEW create_mv_target AS SelectStmt opt_with_data
+				{
+					CreateTableAsStmt *ctas = makeNode(CreateTableAsStmt);
+
+					ctas->query = $9;
+					ctas->into = $7;
+					ctas->objtype = OBJECT_MATVIEW;
+					ctas->is_select_into = false;
+					ctas->if_not_exists = false;
+					/* cram additional flags into the IntoClause */
+					$7->rel->relpersistence = $4;
+					$7->skipData = !($10);
+					$7->replace = true;
+					$$ = (Node *) ctas;
+				}
 		;
 
 create_mv_target:
diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index 891face1b6..f3291e79d8 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -1811,7 +1811,7 @@ psql_completion(const char *text, int start, int end)
 	/* complete with something you can create or replace */
 	else if (TailMatches("CREATE", "OR", "REPLACE"))
 		COMPLETE_WITH("FUNCTION", "PROCEDURE", "LANGUAGE", "RULE", "VIEW",
-					  "AGGREGATE", "TRANSFORM", "TRIGGER");
+					  "AGGREGATE", "TRANSFORM", "TRIGGER", "MATERIALIZED VIEW");
 
 /* DROP, but not DROP embedded in other commands */
 	/* complete with something you can drop */
@@ -3619,13 +3619,16 @@ psql_completion(const char *text, int start, int end)
 		COMPLETE_WITH("SELECT");
 
 /* CREATE MATERIALIZED VIEW */
-	else if (Matches("CREATE", "MATERIALIZED"))
+	else if (Matches("CREATE", "MATERIALIZED") ||
+			 Matches("CREATE", "OR", "REPLACE", "MATERIALIZED"))
 		COMPLETE_WITH("VIEW");
-	/* Complete CREATE MATERIALIZED VIEW <name> with AS */
-	else if (Matches("CREATE", "MATERIALIZED", "VIEW", MatchAny))
+	/* Complete CREATE [ OR REPLACE ] MATERIALIZED VIEW <name> with AS */
+	else if (Matches("CREATE", "MATERIALIZED", "VIEW", MatchAny) ||
+			 Matches("CREATE", "OR", "REPLACE", "MATERIALIZED", "VIEW", MatchAny))
 		COMPLETE_WITH("AS");
-	/* Complete "CREATE MATERIALIZED VIEW <sth> AS with "SELECT" */
-	else if (Matches("CREATE", "MATERIALIZED", "VIEW", MatchAny, "AS"))
+	/* Complete "CREATE [ OR REPLACE ] MATERIALIZED VIEW <sth> AS with "SELECT" */
+	else if (Matches("CREATE", "MATERIALIZED", "VIEW", MatchAny, "AS") ||
+			 Matches("CREATE", "OR", "REPLACE", "MATERIALIZED", "VIEW", MatchAny, "AS"))
 		COMPLETE_WITH("SELECT");
 
 /* CREATE EVENT TRIGGER */
diff --git a/src/include/commands/view.h b/src/include/commands/view.h
index d2d8588989..7eacdaaceb 100644
--- a/src/include/commands/view.h
+++ b/src/include/commands/view.h
@@ -22,4 +22,7 @@ extern ObjectAddress DefineView(ViewStmt *stmt, const char *queryString,
 
 extern void StoreViewQuery(Oid viewOid, Query *viewParse, bool replace);
 
+extern void checkViewColumns(TupleDesc newdesc, TupleDesc olddesc,
+							 bool is_matview);
+
 #endif							/* VIEW_H */
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 85a62b538e..cbc4b608e3 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -2351,7 +2351,7 @@ typedef struct AlterTableStmt
 typedef enum AlterTableType
 {
 	AT_AddColumn,				/* add column */
-	AT_AddColumnToView,			/* implicitly via CREATE OR REPLACE VIEW */
+	AT_AddColumnToView,			/* implicitly via CREATE OR REPLACE [MATERIALIZED] VIEW */
 	AT_ColumnDefault,			/* alter column default */
 	AT_CookedColumnDefault,		/* add a pre-cooked column default */
 	AT_DropNotNull,				/* alter column drop not null */
diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h
index ea47652adb..4b0ee5d10d 100644
--- a/src/include/nodes/primnodes.h
+++ b/src/include/nodes/primnodes.h
@@ -168,6 +168,7 @@ typedef struct IntoClause
 	/* materialized view's SELECT query */
 	Node	   *viewQuery pg_node_attr(query_jumble_ignore);
 	bool		skipData;		/* true for WITH NO DATA */
+	bool		replace;		/* replace existing matview? */
 } IntoClause;
 
 
diff --git a/src/test/regress/expected/matview.out b/src/test/regress/expected/matview.out
index 038ab73517..e2e2a13396 100644
--- a/src/test/regress/expected/matview.out
+++ b/src/test/regress/expected/matview.out
@@ -694,3 +694,194 @@ NOTICE:  relation "matview_ine_tab" already exists, skipping
 (0 rows)
 
 DROP MATERIALIZED VIEW matview_ine_tab;
+--
+-- test CREATE OR REPLACE MATERIALIZED VIEW
+--
+-- matview does not already exist
+DROP MATERIALIZED VIEW IF EXISTS mvtest_replace;
+NOTICE:  materialized view "mvtest_replace" does not exist, skipping
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 1 AS a;
+SELECT * FROM mvtest_replace;
+ a 
+---
+ 1
+(1 row)
+
+-- replace query with data
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 2 AS a;
+SELECT * FROM mvtest_replace;
+ a 
+---
+ 2
+(1 row)
+
+-- replace query without data
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 3 AS a
+  WITH NO DATA;
+SELECT * FROM mvtest_replace; -- error: not populated
+ERROR:  materialized view "mvtest_replace" has not been populated
+HINT:  Use the REFRESH MATERIALIZED VIEW command.
+REFRESH MATERIALIZED VIEW mvtest_replace;
+SELECT * FROM mvtest_replace;
+ a 
+---
+ 3
+(1 row)
+
+-- add column
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 4 AS a, 1 b;
+SELECT * FROM mvtest_replace;
+ a | b 
+---+---
+ 4 | 1
+(1 row)
+
+-- replace table options
+SELECT m.*, c.relname, c.reloptions, s.spcname, a.amname
+  FROM mvtest_replace m
+    CROSS JOIN pg_class c
+    LEFT JOIN pg_tablespace s ON s.oid = c.reltablespace
+    LEFT JOIN pg_am a ON a.oid = c.relam
+  WHERE c.relname = 'mvtest_replace';
+ a | b |    relname     | reloptions | spcname | amname 
+---+---+----------------+------------+---------+--------
+ 4 | 1 | mvtest_replace |            |         | heap
+(1 row)
+
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace
+  USING heap2
+  WITH (fillfactor = 50)
+  TABLESPACE regress_tblspace
+  AS SELECT 5 AS a, 1 AS b;
+SELECT m.*, c.relname, c.reloptions, s.spcname, a.amname
+  FROM mvtest_replace m
+    CROSS JOIN pg_class c
+    LEFT JOIN pg_tablespace s ON s.oid = c.reltablespace
+    LEFT JOIN pg_am a ON a.oid = c.relam
+  WHERE c.relname = 'mvtest_replace';
+ a | b |    relname     |   reloptions    |     spcname      | amname 
+---+---+----------------+-----------------+------------------+--------
+ 5 | 1 | mvtest_replace | {fillfactor=50} | regress_tblspace | heap2
+(1 row)
+
+-- can replace matview that has a dependent view
+CREATE VIEW mvtest_replace_v AS
+  SELECT * FROM mvtest_replace;
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 6 AS a, 1 AS b;
+SELECT * FROM mvtest_replace, mvtest_replace_v;
+ a | b | a | b 
+---+---+---+---
+ 6 | 1 | 6 | 1
+(1 row)
+
+DROP VIEW mvtest_replace_v;
+-- index gets rebuilt when replacing with data
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 7 AS a, 1 AS b;
+CREATE UNIQUE INDEX ON mvtest_replace (b);
+SELECT * FROM mvtest_replace;
+ a | b 
+---+---
+ 7 | 1
+(1 row)
+
+SET enable_seqscan = off; -- force index scan
+EXPLAIN (COSTS OFF) SELECT * FROM mvtest_replace WHERE b = 1;
+                       QUERY PLAN                        
+---------------------------------------------------------
+ Index Scan using mvtest_replace_b_idx on mvtest_replace
+   Index Cond: (b = 1)
+(2 rows)
+
+SELECT * FROM mvtest_replace WHERE b = 1;
+ a | b 
+---+---
+ 7 | 1
+(1 row)
+
+RESET enable_seqscan;
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 8 AS a, 1 AS b;
+SET enable_seqscan = off; -- force index scan
+EXPLAIN (COSTS OFF) SELECT * FROM mvtest_replace WHERE b = 1;
+                       QUERY PLAN                        
+---------------------------------------------------------
+ Index Scan using mvtest_replace_b_idx on mvtest_replace
+   Index Cond: (b = 1)
+(2 rows)
+
+SELECT * FROM mvtest_replace WHERE b = 1;
+ a | b 
+---+---
+ 8 | 1
+(1 row)
+
+RESET enable_seqscan;
+-- cannot change column data type
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 9 AS a, 'x' AS b; -- error
+ERROR:  cannot change data type of materialized view column "b" from integer to text
+SELECT * FROM mvtest_replace;
+ a | b 
+---+---
+ 8 | 1
+(1 row)
+
+-- cannot rename column
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 10 AS a, 1 AS b2; -- error
+ERROR:  cannot change name of materialized view column "b" to "b2"
+HINT:  Use ALTER MATERIALIZED VIEW ... RENAME COLUMN ... to change name of materialized view column instead.
+SELECT * FROM mvtest_replace;
+ a | b 
+---+---
+ 8 | 1
+(1 row)
+
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 11 AS a, 1 AS b, 'y' COLLATE "C" AS c;
+SELECT * FROM mvtest_replace;
+ a  | b | c 
+----+---+---
+ 11 | 1 | y
+(1 row)
+
+-- cannot change column collation
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 12 AS a, 1 AS b, 'x' COLLATE "POSIX" AS c; -- error
+ERROR:  cannot change collation of materialized view column "c" from "C" to "POSIX"
+SELECT * FROM mvtest_replace;
+ a  | b | c 
+----+---+---
+ 11 | 1 | y
+(1 row)
+
+-- cannot drop column
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 13 AS a, 1 AS b; -- error
+ERROR:  cannot drop columns from materialized view
+SELECT * FROM mvtest_replace;
+ a  | b | c 
+----+---+---
+ 11 | 1 | y
+(1 row)
+
+-- must target a matview
+CREATE VIEW mvtest_not_mv AS
+  SELECT 1 AS a;
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_not_mv AS
+  SELECT 1 AS a; -- error
+ERROR:  "mvtest_not_mv" is not a materialized view
+DROP VIEW mvtest_not_mv;
+-- cannot use OR REPLACE with IF NOT EXISTS
+CREATE OR REPLACE MATERIALIZED VIEW IF NOT EXISTS mvtest_replace AS
+  SELECT 1 AS a;
+ERROR:  syntax error at or near "NOT"
+LINE 1: CREATE OR REPLACE MATERIALIZED VIEW IF NOT EXISTS mvtest_rep...
+                                               ^
+DROP MATERIALIZED VIEW mvtest_replace;
diff --git a/src/test/regress/sql/matview.sql b/src/test/regress/sql/matview.sql
index b74ee305e0..c12f0243c9 100644
--- a/src/test/regress/sql/matview.sql
+++ b/src/test/regress/sql/matview.sql
@@ -314,3 +314,111 @@ EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF)
   CREATE MATERIALIZED VIEW IF NOT EXISTS matview_ine_tab AS
     SELECT 1 / 0 WITH NO DATA; -- ok
 DROP MATERIALIZED VIEW matview_ine_tab;
+
+--
+-- test CREATE OR REPLACE MATERIALIZED VIEW
+--
+
+-- matview does not already exist
+DROP MATERIALIZED VIEW IF EXISTS mvtest_replace;
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 1 AS a;
+SELECT * FROM mvtest_replace;
+
+-- replace query with data
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 2 AS a;
+SELECT * FROM mvtest_replace;
+
+-- replace query without data
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 3 AS a
+  WITH NO DATA;
+SELECT * FROM mvtest_replace; -- error: not populated
+REFRESH MATERIALIZED VIEW mvtest_replace;
+SELECT * FROM mvtest_replace;
+
+-- add column
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 4 AS a, 1 b;
+SELECT * FROM mvtest_replace;
+
+-- replace table options
+SELECT m.*, c.relname, c.reloptions, s.spcname, a.amname
+  FROM mvtest_replace m
+    CROSS JOIN pg_class c
+    LEFT JOIN pg_tablespace s ON s.oid = c.reltablespace
+    LEFT JOIN pg_am a ON a.oid = c.relam
+  WHERE c.relname = 'mvtest_replace';
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace
+  USING heap2
+  WITH (fillfactor = 50)
+  TABLESPACE regress_tblspace
+  AS SELECT 5 AS a, 1 AS b;
+SELECT m.*, c.relname, c.reloptions, s.spcname, a.amname
+  FROM mvtest_replace m
+    CROSS JOIN pg_class c
+    LEFT JOIN pg_tablespace s ON s.oid = c.reltablespace
+    LEFT JOIN pg_am a ON a.oid = c.relam
+  WHERE c.relname = 'mvtest_replace';
+
+-- can replace matview that has a dependent view
+CREATE VIEW mvtest_replace_v AS
+  SELECT * FROM mvtest_replace;
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 6 AS a, 1 AS b;
+SELECT * FROM mvtest_replace, mvtest_replace_v;
+DROP VIEW mvtest_replace_v;
+
+-- index gets rebuilt when replacing with data
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 7 AS a, 1 AS b;
+CREATE UNIQUE INDEX ON mvtest_replace (b);
+SELECT * FROM mvtest_replace;
+SET enable_seqscan = off; -- force index scan
+EXPLAIN (COSTS OFF) SELECT * FROM mvtest_replace WHERE b = 1;
+SELECT * FROM mvtest_replace WHERE b = 1;
+RESET enable_seqscan;
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 8 AS a, 1 AS b;
+SET enable_seqscan = off; -- force index scan
+EXPLAIN (COSTS OFF) SELECT * FROM mvtest_replace WHERE b = 1;
+SELECT * FROM mvtest_replace WHERE b = 1;
+RESET enable_seqscan;
+
+-- cannot change column data type
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 9 AS a, 'x' AS b; -- error
+SELECT * FROM mvtest_replace;
+
+-- cannot rename column
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 10 AS a, 1 AS b2; -- error
+SELECT * FROM mvtest_replace;
+
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 11 AS a, 1 AS b, 'y' COLLATE "C" AS c;
+SELECT * FROM mvtest_replace;
+
+-- cannot change column collation
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 12 AS a, 1 AS b, 'x' COLLATE "POSIX" AS c; -- error
+SELECT * FROM mvtest_replace;
+
+-- cannot drop column
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 13 AS a, 1 AS b; -- error
+SELECT * FROM mvtest_replace;
+
+-- must target a matview
+CREATE VIEW mvtest_not_mv AS
+  SELECT 1 AS a;
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_not_mv AS
+  SELECT 1 AS a; -- error
+DROP VIEW mvtest_not_mv;
+
+-- cannot use OR REPLACE with IF NOT EXISTS
+CREATE OR REPLACE MATERIALIZED VIEW IF NOT EXISTS mvtest_replace AS
+  SELECT 1 AS a;
+
+DROP MATERIALIZED VIEW mvtest_replace;
-- 
2.45.2

  [text/x-diff] v2-0002-Deprecate-CREATE-MATERIALIZED-VIEW-IF-NOT-EXISTS.patch (8.5K, ../../7afe68b0-f983-4a9f-a1b4-32188cebbf38@ewie.name/3-v2-0002-Deprecate-CREATE-MATERIALIZED-VIEW-IF-NOT-EXISTS.patch)
  download | inline diff:
From f0ed9b74e50530abefb2edc51603c8df061eaa22 Mon Sep 17 00:00:00 2001
From: Erik Wienhold <ewie@ewie.name>
Date: Tue, 28 May 2024 02:19:53 +0200
Subject: [PATCH v2 2/3] Deprecate CREATE MATERIALIZED VIEW IF NOT EXISTS

---
 src/backend/parser/gram.y                     | 14 +++++++++++++
 .../expected/test_extensions.out              | 18 +++++++++++++++++
 src/test/regress/expected/matview.out         | 20 +++++++++++++++++++
 3 files changed, 52 insertions(+)

diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index ed477806d1..b1100bdec1 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -4830,6 +4830,20 @@ CreateMatViewStmt:
 					$8->rel->relpersistence = $2;
 					$8->skipData = !($11);
 					$$ = (Node *) ctas;
+
+					if (ctas->into->rel->schemaname)
+							ereport(WARNING,
+									errmsg("IF NOT EXISTS is deprecated in materialized view creation"),
+									errhint("Use CREATE OR REPLACE MATERIALIZED VIEW %s.%s.",
+											ctas->into->rel->schemaname,
+											ctas->into->rel->relname),
+									parser_errposition(@1));
+					else
+							ereport(WARNING,
+									errmsg("IF NOT EXISTS is deprecated in materialized view creation"),
+									errhint("Use CREATE OR REPLACE MATERIALIZED VIEW %s.",
+											ctas->into->rel->relname),
+									parser_errposition(@1));
 				}
 		| CREATE OR REPLACE OptNoLog MATERIALIZED VIEW create_mv_target AS SelectStmt opt_with_data
 				{
diff --git a/src/test/modules/test_extensions/expected/test_extensions.out b/src/test/modules/test_extensions/expected/test_extensions.out
index f357cc21aa..ecd453b29d 100644
--- a/src/test/modules/test_extensions/expected/test_extensions.out
+++ b/src/test/modules/test_extensions/expected/test_extensions.out
@@ -377,41 +377,57 @@ Objects in extension "test_ext_cor"
 CREATE COLLATION ext_cine_coll
   ( LC_COLLATE = "C", LC_CTYPE = "C" );
 CREATE EXTENSION test_ext_cine;  -- fail
+WARNING:  IF NOT EXISTS is deprecated in materialized view creation
+HINT:  Use CREATE OR REPLACE MATERIALIZED VIEW ext_cine_mv.
 ERROR:  collation ext_cine_coll is not a member of extension "test_ext_cine"
 DETAIL:  An extension may only use CREATE ... IF NOT EXISTS to skip object creation if the conflicting object is one that it already owns.
 DROP COLLATION ext_cine_coll;
 CREATE MATERIALIZED VIEW ext_cine_mv AS SELECT 11 AS f1;
 CREATE EXTENSION test_ext_cine;  -- fail
+WARNING:  IF NOT EXISTS is deprecated in materialized view creation
+HINT:  Use CREATE OR REPLACE MATERIALIZED VIEW ext_cine_mv.
 ERROR:  materialized view ext_cine_mv is not a member of extension "test_ext_cine"
 DETAIL:  An extension may only use CREATE ... IF NOT EXISTS to skip object creation if the conflicting object is one that it already owns.
 DROP MATERIALIZED VIEW ext_cine_mv;
 CREATE FOREIGN DATA WRAPPER dummy;
 CREATE SERVER ext_cine_srv FOREIGN DATA WRAPPER dummy;
 CREATE EXTENSION test_ext_cine;  -- fail
+WARNING:  IF NOT EXISTS is deprecated in materialized view creation
+HINT:  Use CREATE OR REPLACE MATERIALIZED VIEW ext_cine_mv.
 ERROR:  server ext_cine_srv is not a member of extension "test_ext_cine"
 DETAIL:  An extension may only use CREATE ... IF NOT EXISTS to skip object creation if the conflicting object is one that it already owns.
 DROP SERVER ext_cine_srv;
 CREATE SCHEMA ext_cine_schema;
 CREATE EXTENSION test_ext_cine;  -- fail
+WARNING:  IF NOT EXISTS is deprecated in materialized view creation
+HINT:  Use CREATE OR REPLACE MATERIALIZED VIEW ext_cine_mv.
 ERROR:  schema ext_cine_schema is not a member of extension "test_ext_cine"
 DETAIL:  An extension may only use CREATE ... IF NOT EXISTS to skip object creation if the conflicting object is one that it already owns.
 DROP SCHEMA ext_cine_schema;
 CREATE SEQUENCE ext_cine_seq;
 CREATE EXTENSION test_ext_cine;  -- fail
+WARNING:  IF NOT EXISTS is deprecated in materialized view creation
+HINT:  Use CREATE OR REPLACE MATERIALIZED VIEW ext_cine_mv.
 ERROR:  sequence ext_cine_seq is not a member of extension "test_ext_cine"
 DETAIL:  An extension may only use CREATE ... IF NOT EXISTS to skip object creation if the conflicting object is one that it already owns.
 DROP SEQUENCE ext_cine_seq;
 CREATE TABLE ext_cine_tab1 (x int);
 CREATE EXTENSION test_ext_cine;  -- fail
+WARNING:  IF NOT EXISTS is deprecated in materialized view creation
+HINT:  Use CREATE OR REPLACE MATERIALIZED VIEW ext_cine_mv.
 ERROR:  table ext_cine_tab1 is not a member of extension "test_ext_cine"
 DETAIL:  An extension may only use CREATE ... IF NOT EXISTS to skip object creation if the conflicting object is one that it already owns.
 DROP TABLE ext_cine_tab1;
 CREATE TABLE ext_cine_tab2 AS SELECT 42 AS y;
 CREATE EXTENSION test_ext_cine;  -- fail
+WARNING:  IF NOT EXISTS is deprecated in materialized view creation
+HINT:  Use CREATE OR REPLACE MATERIALIZED VIEW ext_cine_mv.
 ERROR:  table ext_cine_tab2 is not a member of extension "test_ext_cine"
 DETAIL:  An extension may only use CREATE ... IF NOT EXISTS to skip object creation if the conflicting object is one that it already owns.
 DROP TABLE ext_cine_tab2;
 CREATE EXTENSION test_ext_cine;
+WARNING:  IF NOT EXISTS is deprecated in materialized view creation
+HINT:  Use CREATE OR REPLACE MATERIALIZED VIEW ext_cine_mv.
 \dx+ test_ext_cine
 Objects in extension "test_ext_cine"
         Object description         
@@ -433,6 +449,8 @@ Objects in extension "test_ext_cine"
 (14 rows)
 
 ALTER EXTENSION test_ext_cine UPDATE TO '1.1';
+WARNING:  IF NOT EXISTS is deprecated in materialized view creation
+HINT:  Use CREATE OR REPLACE MATERIALIZED VIEW ext_cine_mv.
 \dx+ test_ext_cine
 Objects in extension "test_ext_cine"
         Object description         
diff --git a/src/test/regress/expected/matview.out b/src/test/regress/expected/matview.out
index e2e2a13396..cefd0d442c 100644
--- a/src/test/regress/expected/matview.out
+++ b/src/test/regress/expected/matview.out
@@ -565,6 +565,10 @@ CREATE MATERIALIZED VIEW mvtest_mv_foo AS SELECT * FROM mvtest_foo_data;
 CREATE MATERIALIZED VIEW mvtest_mv_foo AS SELECT * FROM mvtest_foo_data;
 ERROR:  relation "mvtest_mv_foo" already exists
 CREATE MATERIALIZED VIEW IF NOT EXISTS mvtest_mv_foo AS SELECT * FROM mvtest_foo_data;
+WARNING:  IF NOT EXISTS is deprecated in materialized view creation
+LINE 1: CREATE MATERIALIZED VIEW IF NOT EXISTS mvtest_mv_foo AS SELE...
+        ^
+HINT:  Use CREATE OR REPLACE MATERIALIZED VIEW mvtest_mv_foo.
 NOTICE:  relation "mvtest_mv_foo" already exists, skipping
 CREATE UNIQUE INDEX ON mvtest_mv_foo (i);
 RESET ROLE;
@@ -662,12 +666,20 @@ CREATE MATERIALIZED VIEW matview_ine_tab AS SELECT 1 / 0; -- error
 ERROR:  relation "matview_ine_tab" already exists
 CREATE MATERIALIZED VIEW IF NOT EXISTS matview_ine_tab AS
   SELECT 1 / 0; -- ok
+WARNING:  IF NOT EXISTS is deprecated in materialized view creation
+LINE 1: CREATE MATERIALIZED VIEW IF NOT EXISTS matview_ine_tab AS
+        ^
+HINT:  Use CREATE OR REPLACE MATERIALIZED VIEW matview_ine_tab.
 NOTICE:  relation "matview_ine_tab" already exists, skipping
 CREATE MATERIALIZED VIEW matview_ine_tab AS
   SELECT 1 / 0 WITH NO DATA; -- error
 ERROR:  relation "matview_ine_tab" already exists
 CREATE MATERIALIZED VIEW IF NOT EXISTS matview_ine_tab AS
   SELECT 1 / 0 WITH NO DATA; -- ok
+WARNING:  IF NOT EXISTS is deprecated in materialized view creation
+LINE 1: CREATE MATERIALIZED VIEW IF NOT EXISTS matview_ine_tab AS
+        ^
+HINT:  Use CREATE OR REPLACE MATERIALIZED VIEW matview_ine_tab.
 NOTICE:  relation "matview_ine_tab" already exists, skipping
 EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF)
   CREATE MATERIALIZED VIEW matview_ine_tab AS
@@ -676,6 +688,10 @@ ERROR:  relation "matview_ine_tab" already exists
 EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF)
   CREATE MATERIALIZED VIEW IF NOT EXISTS matview_ine_tab AS
     SELECT 1 / 0; -- ok
+WARNING:  IF NOT EXISTS is deprecated in materialized view creation
+LINE 2:   CREATE MATERIALIZED VIEW IF NOT EXISTS matview_ine_tab AS
+          ^
+HINT:  Use CREATE OR REPLACE MATERIALIZED VIEW matview_ine_tab.
 NOTICE:  relation "matview_ine_tab" already exists, skipping
  QUERY PLAN 
 ------------
@@ -688,6 +704,10 @@ ERROR:  relation "matview_ine_tab" already exists
 EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF)
   CREATE MATERIALIZED VIEW IF NOT EXISTS matview_ine_tab AS
     SELECT 1 / 0 WITH NO DATA; -- ok
+WARNING:  IF NOT EXISTS is deprecated in materialized view creation
+LINE 2:   CREATE MATERIALIZED VIEW IF NOT EXISTS matview_ine_tab AS
+          ^
+HINT:  Use CREATE OR REPLACE MATERIALIZED VIEW matview_ine_tab.
 NOTICE:  relation "matview_ine_tab" already exists, skipping
  QUERY PLAN 
 ------------
-- 
2.45.2

  [text/x-diff] v2-0003-Replace-matview-WITH-OLD-DATA.patch (7.7K, ../../7afe68b0-f983-4a9f-a1b4-32188cebbf38@ewie.name/4-v2-0003-Replace-matview-WITH-OLD-DATA.patch)
  download | inline diff:
From c386742310dcf2429b796c2a2d32f12db2cc419f Mon Sep 17 00:00:00 2001
From: Erik Wienhold <ewie@ewie.name>
Date: Fri, 26 Jul 2024 23:33:15 +0200
Subject: [PATCH v2 3/3] Replace matview WITH OLD DATA

---
 .../sgml/ref/create_materialized_view.sgml    | 16 +++++++++--
 src/backend/commands/createas.c               | 26 +++++++++++------
 src/backend/parser/gram.y                     | 16 +++++++++++
 src/include/nodes/primnodes.h                 |  1 +
 src/test/regress/expected/matview.out         | 28 +++++++++++++++++++
 src/test/regress/sql/matview.sql              | 15 ++++++++++
 6 files changed, 90 insertions(+), 12 deletions(-)

diff --git a/doc/src/sgml/ref/create_materialized_view.sgml b/doc/src/sgml/ref/create_materialized_view.sgml
index b5a8e3441a..65633b8bfa 100644
--- a/doc/src/sgml/ref/create_materialized_view.sgml
+++ b/doc/src/sgml/ref/create_materialized_view.sgml
@@ -27,7 +27,7 @@ CREATE [ OR REPLACE ] MATERIALIZED VIEW [ IF NOT EXISTS ] <replaceable>table_nam
     [ WITH ( <replaceable class="parameter">storage_parameter</replaceable> [= <replaceable class="parameter">value</replaceable>] [, ... ] ) ]
     [ TABLESPACE <replaceable class="parameter">tablespace_name</replaceable> ]
     AS <replaceable>query</replaceable>
-    [ WITH [ NO ] DATA ]
+    [ WITH [ NO | OLD ] DATA ]
 </synopsis>
  </refsynopsisdiv>
 
@@ -37,7 +37,8 @@ CREATE [ OR REPLACE ] MATERIALIZED VIEW [ IF NOT EXISTS ] <replaceable>table_nam
   <para>
    <command>CREATE MATERIALIZED VIEW</command> defines a materialized view of
    a query.  The query is executed and used to populate the view at the time
-   the command is issued (unless <command>WITH NO DATA</command> is used) and may be
+   the command is issued (unless <command>WITH NO DATA</command> or
+   <command>WITH OLD DATA</command> is used) and may be
    refreshed later using <command>REFRESH MATERIALIZED VIEW</command>.
   </para>
 
@@ -160,7 +161,7 @@ CREATE [ OR REPLACE ] MATERIALIZED VIEW [ IF NOT EXISTS ] <replaceable>table_nam
    </varlistentry>
 
    <varlistentry>
-    <term><literal>WITH [ NO ] DATA</literal></term>
+    <term><literal>WITH [ NO | OLD ] DATA</literal></term>
     <listitem>
      <para>
       This clause specifies whether or not the materialized view should be
@@ -168,6 +169,15 @@ CREATE [ OR REPLACE ] MATERIALIZED VIEW [ IF NOT EXISTS ] <replaceable>table_nam
       flagged as unscannable and cannot be queried until <command>REFRESH
       MATERIALIZED VIEW</command> is used.
      </para>
+
+     <para>
+      The form <command>WITH OLD DATA</command> keeps the already stored data
+      when replacing an existing materialized view to keep it populated.  For
+      newly created materialized views, this has the same effect as
+      <command>WITH DATA</command>.  Use this form if you want to use
+      <command>REFRESH MATERIALIZED VIEW CONCURRENTLY</command> as it requires
+      a populated materialized view.
+     </para>
     </listitem>
    </varlistentry>
 
diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c
index c5d78252a1..e2948ac966 100644
--- a/src/backend/commands/createas.c
+++ b/src/backend/commands/createas.c
@@ -334,18 +334,26 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt,
 		/* An existing materialized view can be replaced. */
 		if (is_matview && into->replace)
 		{
-			RefreshMatViewStmt *refresh;
-
 			/* Change the relation to match the new query and other options. */
-			(void) create_ctas_nodata(query->targetList, into);
+			address = create_ctas_nodata(query->targetList, into);
 
-			/* Refresh the materialized view with a fake statement. */
-			refresh = makeNode(RefreshMatViewStmt);
-			refresh->relation = into->rel;
-			refresh->skipData = into->skipData;
-			refresh->concurrent = false;
+			/*
+			 * Refresh the materialized view with a fake statement unless we
+			 * must keep the old data.
+			 */
+			if (!into->keepData)
+			{
+				RefreshMatViewStmt *refresh;
+
+				refresh = makeNode(RefreshMatViewStmt);
+				refresh->relation = into->rel;
+				refresh->skipData = into->skipData;
+				refresh->concurrent = false;
+
+				address = ExecRefreshMatView(refresh, NULL, NULL, NULL);
+			}
 
-			return ExecRefreshMatView(refresh, NULL, NULL, NULL);
+			return address;
 		}
 
 		return InvalidObjectAddress;
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index b1100bdec1..0647955005 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -4860,6 +4860,22 @@ CreateMatViewStmt:
 					$7->replace = true;
 					$$ = (Node *) ctas;
 				}
+		| CREATE OR REPLACE OptNoLog MATERIALIZED VIEW create_mv_target AS SelectStmt WITH OLD DATA_P
+				{
+					CreateTableAsStmt *ctas = makeNode(CreateTableAsStmt);
+
+					ctas->query = $9;
+					ctas->into = $7;
+					ctas->objtype = OBJECT_MATVIEW;
+					ctas->is_select_into = false;
+					ctas->if_not_exists = false;
+					/* cram additional flags into the IntoClause */
+					$7->rel->relpersistence = $4;
+					$7->skipData = false;
+					$7->keepData = true;
+					$7->replace = true;
+					$$ = (Node *) ctas;
+				}
 		;
 
 create_mv_target:
diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h
index 4b0ee5d10d..ae84cb522e 100644
--- a/src/include/nodes/primnodes.h
+++ b/src/include/nodes/primnodes.h
@@ -168,6 +168,7 @@ typedef struct IntoClause
 	/* materialized view's SELECT query */
 	Node	   *viewQuery pg_node_attr(query_jumble_ignore);
 	bool		skipData;		/* true for WITH NO DATA */
+	bool		keepData;		/* true for WITH OLD DATA */
 	bool		replace;		/* replace existing matview? */
 } IntoClause;
 
diff --git a/src/test/regress/expected/matview.out b/src/test/regress/expected/matview.out
index cefd0d442c..47dfd88bff 100644
--- a/src/test/regress/expected/matview.out
+++ b/src/test/regress/expected/matview.out
@@ -751,6 +751,23 @@ SELECT * FROM mvtest_replace;
  3
 (1 row)
 
+-- replace query but keep old data
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 5 AS a
+  WITH OLD DATA;
+SELECT * FROM mvtest_replace;
+ a 
+---
+ 3
+(1 row)
+
+REFRESH MATERIALIZED VIEW mvtest_replace;
+SELECT * FROM mvtest_replace;
+ a 
+---
+ 5
+(1 row)
+
 -- add column
 CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
   SELECT 4 AS a, 1 b;
@@ -905,3 +922,14 @@ ERROR:  syntax error at or near "NOT"
 LINE 1: CREATE OR REPLACE MATERIALIZED VIEW IF NOT EXISTS mvtest_rep...
                                                ^
 DROP MATERIALIZED VIEW mvtest_replace;
+-- Create new matview WITH OLD DATA.  This populates the new matview as if
+-- WITH DATA had been specified.
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 17 AS a
+  WITH OLD DATA;
+SELECT * FROM mvtest_replace;
+ a  
+----
+ 17
+(1 row)
+
diff --git a/src/test/regress/sql/matview.sql b/src/test/regress/sql/matview.sql
index c12f0243c9..b268237c24 100644
--- a/src/test/regress/sql/matview.sql
+++ b/src/test/regress/sql/matview.sql
@@ -338,6 +338,14 @@ SELECT * FROM mvtest_replace; -- error: not populated
 REFRESH MATERIALIZED VIEW mvtest_replace;
 SELECT * FROM mvtest_replace;
 
+-- replace query but keep old data
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 5 AS a
+  WITH OLD DATA;
+SELECT * FROM mvtest_replace;
+REFRESH MATERIALIZED VIEW mvtest_replace;
+SELECT * FROM mvtest_replace;
+
 -- add column
 CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
   SELECT 4 AS a, 1 b;
@@ -422,3 +430,10 @@ CREATE OR REPLACE MATERIALIZED VIEW IF NOT EXISTS mvtest_replace AS
   SELECT 1 AS a;
 
 DROP MATERIALIZED VIEW mvtest_replace;
+
+-- Create new matview WITH OLD DATA.  This populates the new matview as if
+-- WITH DATA had been specified.
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 17 AS a
+  WITH OLD DATA;
+SELECT * FROM mvtest_replace;
-- 
2.45.2

^ permalink  raw  reply  [nested|flat] 29+ messages in thread

* Re: CREATE OR REPLACE MATERIALIZED VIEW
@ 2024-09-05 20:33  Erik Wienhold <ewie@ewie.name>
  parent: Erik Wienhold <ewie@ewie.name>
  0 siblings, 1 reply; 29+ messages in thread

From: Erik Wienhold @ 2024-09-05 20:33 UTC (permalink / raw)
  To: Said Assemlal <sassemlal@neurorx.com>; +Cc: pgsql-hackers

On 2024-07-27 02:45 +0200, Erik Wienhold wrote:
> On 2024-07-12 16:49 +0200, Said Assemlal wrote:
> > > My initial idea, while writing the patch, was that one could replace the
> > > matview without populating it and then run the concurrent refresh, like
> > > this:
> > > 
> > >      CREATE OR REPLACE MATERIALIZED VIEW foo AS ... WITH NO DATA;
> > >      REFRESH MATERIALIZED VIEW CONCURRENTLY foo;
> > > 
> > > But that won't work because concurrent refresh requires an already
> > > populated matview.
> > > 
> > > Right now the patch either populates the replaced matview or leaves it
> > > in an unscannable state.  Technically, it's also possible to skip the
> > > refresh and leave the old data in place, perhaps by specifying
> > > WITH *OLD* DATA.  New columns would just be null.  Of course you can't
> > > tell if you got stale data without knowing how the matview was replaced.
> > > Thoughts?
> > 
> > I believe the expectation is to get materialized views updated whenever it
> > gets replaced so likely to confuse users ?
> 
> I agree, that could be confusing -- unless it's well documented.  The
> attached 0003 implements WITH OLD DATA and states in the docs that this
> is intended to be used before a concurrent refresh.
> 
> Patch 0001 now covers all matview cases in psql's tab completion.  I
> missed some of them with v1.

Here's a rebased version due to conflicts with f683d3a4ca and
1e35951e71.  No other changes since v2.

-- 
Erik

Attachments:

  [text/x-diff] v3-0001-Add-CREATE-OR-REPLACE-MATERIALIZED-VIEW.patch (31.9K, ../../b74736cf-a4b4-4c32-8df1-d08abe9a14ed@ewie.name/2-v3-0001-Add-CREATE-OR-REPLACE-MATERIALIZED-VIEW.patch)
  download | inline diff:
From 158f025fa2696a4a807def4a3c533982cfd7b318 Mon Sep 17 00:00:00 2001
From: Erik Wienhold <ewie@ewie.name>
Date: Tue, 21 May 2024 18:35:47 +0200
Subject: [PATCH v3 1/3] Add CREATE OR REPLACE MATERIALIZED VIEW

---
 .../sgml/ref/create_materialized_view.sgml    |  15 +-
 src/backend/commands/createas.c               | 207 ++++++++++++++----
 src/backend/commands/tablecmds.c              |   8 +-
 src/backend/commands/view.c                   | 106 ++++++---
 src/backend/parser/gram.y                     |  15 ++
 src/bin/psql/tab-complete.c                   |  15 +-
 src/include/commands/view.h                   |   3 +
 src/include/nodes/parsenodes.h                |   2 +-
 src/include/nodes/primnodes.h                 |   1 +
 src/test/regress/expected/matview.out         | 191 ++++++++++++++++
 src/test/regress/sql/matview.sql              | 108 +++++++++
 11 files changed, 582 insertions(+), 89 deletions(-)

diff --git a/doc/src/sgml/ref/create_materialized_view.sgml b/doc/src/sgml/ref/create_materialized_view.sgml
index 0d2fea2b97..b5a8e3441a 100644
--- a/doc/src/sgml/ref/create_materialized_view.sgml
+++ b/doc/src/sgml/ref/create_materialized_view.sgml
@@ -21,7 +21,7 @@ PostgreSQL documentation
 
  <refsynopsisdiv>
 <synopsis>
-CREATE MATERIALIZED VIEW [ IF NOT EXISTS ] <replaceable>table_name</replaceable>
+CREATE [ OR REPLACE ] MATERIALIZED VIEW [ IF NOT EXISTS ] <replaceable>table_name</replaceable>
     [ (<replaceable>column_name</replaceable> [, ...] ) ]
     [ USING <replaceable class="parameter">method</replaceable> ]
     [ WITH ( <replaceable class="parameter">storage_parameter</replaceable> [= <replaceable class="parameter">value</replaceable>] [, ... ] ) ]
@@ -60,6 +60,17 @@ CREATE MATERIALIZED VIEW [ IF NOT EXISTS ] <replaceable>table_name</replaceable>
   <title>Parameters</title>
 
   <variablelist>
+   <varlistentry>
+    <term><literal>OR REPLACE</literal></term>
+    <listitem>
+     <para>
+      Replaces a materialized view if it already exists.
+      Specifying <literal>OR REPLACE</literal> together with
+      <literal>IF NOT EXISTS</literal> is an error.
+     </para>
+    </listitem>
+   </varlistentry>
+
    <varlistentry>
     <term><literal>IF NOT EXISTS</literal></term>
     <listitem>
@@ -67,7 +78,7 @@ CREATE MATERIALIZED VIEW [ IF NOT EXISTS ] <replaceable>table_name</replaceable>
       Do not throw an error if a materialized view with the same name already
       exists. A notice is issued in this case.  Note that there is no guarantee
       that the existing materialized view is anything like the one that would
-      have been created.
+      have been created, unless you use <literal>OR REPLACE</literal> instead.
      </para>
     </listitem>
    </varlistentry>
diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c
index 0b629b1f79..e4ed3748f9 100644
--- a/src/backend/commands/createas.c
+++ b/src/backend/commands/createas.c
@@ -79,55 +79,151 @@ static void intorel_destroy(DestReceiver *self);
 static ObjectAddress
 create_ctas_internal(List *attrList, IntoClause *into)
 {
-	CreateStmt *create = makeNode(CreateStmt);
-	bool		is_matview;
+	bool		is_matview,
+				replace = false;
 	char		relkind;
-	Datum		toast_options;
-	const char *const validnsps[] = HEAP_RELOPT_NAMESPACES;
+	Oid			matviewOid = InvalidOid;
 	ObjectAddress intoRelationAddr;
 
 	/* This code supports both CREATE TABLE AS and CREATE MATERIALIZED VIEW */
 	is_matview = (into->viewQuery != NULL);
 	relkind = is_matview ? RELKIND_MATVIEW : RELKIND_RELATION;
 
-	/*
-	 * Create the target relation by faking up a CREATE TABLE parsetree and
-	 * passing it to DefineRelation.
-	 */
-	create->relation = into->rel;
-	create->tableElts = attrList;
-	create->inhRelations = NIL;
-	create->ofTypename = NULL;
-	create->constraints = NIL;
-	create->options = into->options;
-	create->oncommit = into->onCommit;
-	create->tablespacename = into->tableSpaceName;
-	create->if_not_exists = false;
-	create->accessMethod = into->accessMethod;
+	/* Check if an existing materialized view needs to be replaced. */
+	if (is_matview)
+	{
+		LOCKMODE	lockmode;
 
-	/*
-	 * Create the relation.  (This will error out if there's an existing view,
-	 * so we don't need more code to complain if "replace" is false.)
-	 */
-	intoRelationAddr = DefineRelation(create, relkind, InvalidOid, NULL, NULL);
+		lockmode = into->replace ? AccessExclusiveLock : NoLock;
+		(void) RangeVarGetAndCheckCreationNamespace(into->rel, lockmode,
+													&matviewOid);
+		replace = OidIsValid(matviewOid) && into->replace;
+	}
 
-	/*
-	 * If necessary, create a TOAST table for the target table.  Note that
-	 * NewRelationCreateToastTable ends with CommandCounterIncrement(), so
-	 * that the TOAST table will be visible for insertion.
-	 */
-	CommandCounterIncrement();
+	if (is_matview && replace)
+	{
+		Relation	rel;
+		List	   *atcmds = NIL;
+		AlterTableCmd *atcmd;
+		TupleDesc	descriptor;
+
+		rel = relation_open(matviewOid, NoLock);
+
+		if (rel->rd_rel->relkind != RELKIND_MATVIEW)
+			ereport(ERROR,
+					errcode(ERRCODE_WRONG_OBJECT_TYPE),
+					errmsg("\"%s\" is not a materialized view",
+						   RelationGetRelationName(rel)));
+
+		CheckTableNotInUse(rel, "CREATE OR REPLACE MATERIALIZED VIEW");
+
+		descriptor = BuildDescForRelation(attrList);
+		checkViewColumns(descriptor, rel->rd_att, true);
+
+		/* Add new attributes via ALTER TABLE. */
+		if (list_length(attrList) > rel->rd_att->natts)
+		{
+			ListCell   *c;
+			int			skip = rel->rd_att->natts;
+
+			foreach(c, attrList)
+			{
+				if (skip > 0)
+				{
+					skip--;
+					continue;
+				}
+				atcmd = makeNode(AlterTableCmd);
+				atcmd->subtype = AT_AddColumnToView;
+				atcmd->def = (Node *) lfirst(c);
+				atcmds = lappend(atcmds, atcmd);
+			}
+		}
+
+		/* Set access method via ALTER TABLE. */
+		if (into->accessMethod != NULL)
+		{
+			atcmd = makeNode(AlterTableCmd);
+			atcmd->subtype = AT_SetAccessMethod;
+			atcmd->name = into->accessMethod;
+			atcmds = lappend(atcmds, atcmd);
+		}
+
+		/* Set tablespace via ALTER TABLE. */
+		if (into->tableSpaceName != NULL)
+		{
+			atcmd = makeNode(AlterTableCmd);
+			atcmd->subtype = AT_SetTableSpace;
+			atcmd->name = into->tableSpaceName;
+			atcmds = lappend(atcmds, atcmd);
+		}
+
+		/* Set storage parameters via ALTER TABLE. */
+		if (into->options != NIL)
+		{
+			atcmd = makeNode(AlterTableCmd);
+			atcmd->subtype = AT_ReplaceRelOptions;
+			atcmd->def = (Node *) into->options;
+			atcmds = lappend(atcmds, atcmd);
+		}
+
+		if (atcmds != NIL)
+		{
+			AlterTableInternal(matviewOid, atcmds, true);
+			CommandCounterIncrement();
+		}
+
+		relation_close(rel, NoLock);
+
+		ObjectAddressSet(intoRelationAddr, RelationRelationId, matviewOid);
+	}
+	else
+	{
+		CreateStmt *create = makeNode(CreateStmt);
+		Datum		toast_options;
+		const static char *validnsps[] = HEAP_RELOPT_NAMESPACES;
+
+		/*
+		 * Create the target relation by faking up a CREATE TABLE parsetree
+		 * and passing it to DefineRelation.
+		 */
+		create->relation = into->rel;
+		create->tableElts = attrList;
+		create->inhRelations = NIL;
+		create->ofTypename = NULL;
+		create->constraints = NIL;
+		create->options = into->options;
+		create->oncommit = into->onCommit;
+		create->tablespacename = into->tableSpaceName;
+		create->if_not_exists = false;
+		create->accessMethod = into->accessMethod;
+
+		/*
+		 * Create the relation.  (This will error out if there's an existing
+		 * view, so we don't need more code to complain if "replace" is
+		 * false.)
+		 */
+		intoRelationAddr = DefineRelation(create, relkind, InvalidOid, NULL,
+										  NULL);
 
-	/* parse and validate reloptions for the toast table */
-	toast_options = transformRelOptions((Datum) 0,
-										create->options,
-										"toast",
-										validnsps,
-										true, false);
+		/*
+		 * If necessary, create a TOAST table for the target table.  Note that
+		 * NewRelationCreateToastTable ends with CommandCounterIncrement(), so
+		 * that the TOAST table will be visible for insertion.
+		 */
+		CommandCounterIncrement();
+
+		/* parse and validate reloptions for the toast table */
+		toast_options = transformRelOptions((Datum) 0,
+											create->options,
+											"toast",
+											validnsps,
+											true, false);
 
-	(void) heap_reloptions(RELKIND_TOASTVALUE, toast_options, true);
+		(void) heap_reloptions(RELKIND_TOASTVALUE, toast_options, true);
 
-	NewRelationCreateToastTable(intoRelationAddr.objectId, toast_options);
+		NewRelationCreateToastTable(intoRelationAddr.objectId, toast_options);
+	}
 
 	/* Create the "view" part of a materialized view. */
 	if (is_matview)
@@ -135,7 +231,7 @@ create_ctas_internal(List *attrList, IntoClause *into)
 		/* StoreViewQuery scribbles on tree, so make a copy */
 		Query	   *query = (Query *) copyObject(into->viewQuery);
 
-		StoreViewQuery(intoRelationAddr.objectId, query, false);
+		StoreViewQuery(intoRelationAddr.objectId, query, replace);
 		CommandCounterIncrement();
 	}
 
@@ -231,7 +327,26 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt,
 
 	/* Check if the relation exists or not */
 	if (CreateTableAsRelExists(stmt))
+	{
+		/* An existing materialized view can be replaced. */
+		if (is_matview && into->replace)
+		{
+			RefreshMatViewStmt *refresh;
+
+			/* Change the relation to match the new query and other options. */
+			(void) create_ctas_nodata(query->targetList, into);
+
+			/* Refresh the materialized view with a fake statement. */
+			refresh = makeNode(RefreshMatViewStmt);
+			refresh->relation = into->rel;
+			refresh->skipData = into->skipData;
+			refresh->concurrent = false;
+
+			return ExecRefreshMatView(refresh, NULL, NULL);
+		}
+
 		return InvalidObjectAddress;
+	}
 
 	/*
 	 * Create the tuple receiver object and insert info it will need
@@ -392,14 +507,15 @@ CreateTableAsRelExists(CreateTableAsStmt *ctas)
 	oldrelid = get_relname_relid(into->rel->relname, nspid);
 	if (OidIsValid(oldrelid))
 	{
-		if (!ctas->if_not_exists)
+		if (!ctas->if_not_exists && !into->replace)
 			ereport(ERROR,
 					(errcode(ERRCODE_DUPLICATE_TABLE),
 					 errmsg("relation \"%s\" already exists",
 							into->rel->relname)));
 
 		/*
-		 * The relation exists and IF NOT EXISTS has been specified.
+		 * The relation exists and IF NOT EXISTS or OR REPLACE has been
+		 * specified.
 		 *
 		 * If we are in an extension script, insist that the pre-existing
 		 * object be a member of the extension, to avoid security risks.
@@ -407,11 +523,12 @@ CreateTableAsRelExists(CreateTableAsStmt *ctas)
 		ObjectAddressSet(address, RelationRelationId, oldrelid);
 		checkMembershipInCurrentExtension(&address);
 
-		/* OK to skip */
-		ereport(NOTICE,
-				(errcode(ERRCODE_DUPLICATE_TABLE),
-				 errmsg("relation \"%s\" already exists, skipping",
-						into->rel->relname)));
+		if (ctas->if_not_exists)
+			/* OK to skip */
+			ereport(NOTICE,
+					(errcode(ERRCODE_DUPLICATE_TABLE),
+					 errmsg("relation \"%s\" already exists, skipping",
+							into->rel->relname)));
 		return true;
 	}
 
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index b3cc6f8f69..bcc08f9420 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -4480,7 +4480,7 @@ AlterTableGetLockLevel(List *cmds)
 				 * Subcommands that may be visible to concurrent SELECTs
 				 */
 			case AT_DropColumn: /* change visible to SELECT */
-			case AT_AddColumnToView:	/* CREATE VIEW */
+			case AT_AddColumnToView:	/* via CREATE OR REPLACE [MATERIALIZED] VIEW */
 			case AT_DropOids:	/* used to equiv to DropColumn */
 			case AT_EnableAlwaysRule:	/* may change SELECT rules */
 			case AT_EnableReplicaRule:	/* may change SELECT rules */
@@ -4783,8 +4783,8 @@ ATPrepCmd(List **wqueue, Relation rel, AlterTableCmd *cmd,
 			/* Recursion occurs during execution phase */
 			pass = AT_PASS_ADD_COL;
 			break;
-		case AT_AddColumnToView:	/* add column via CREATE OR REPLACE VIEW */
-			ATSimplePermissions(cmd->subtype, rel, ATT_VIEW);
+		case AT_AddColumnToView:	/* via CREATE OR REPLACE [MATERIALIZED] VIEW */
+			ATSimplePermissions(cmd->subtype, rel, ATT_VIEW | ATT_MATVIEW);
 			ATPrepAddColumn(wqueue, rel, recurse, recursing, true, cmd,
 							lockmode, context);
 			/* Recursion occurs during execution phase */
@@ -5178,7 +5178,7 @@ ATExecCmd(List **wqueue, AlteredTableInfo *tab,
 	switch (cmd->subtype)
 	{
 		case AT_AddColumn:		/* ADD COLUMN */
-		case AT_AddColumnToView:	/* add column via CREATE OR REPLACE VIEW */
+		case AT_AddColumnToView:	/* via CREATE OR REPLACE [MATERIALIZED] VIEW */
 			address = ATExecAddColumn(wqueue, tab, rel, &cmd,
 									  cmd->recurse, false,
 									  lockmode, cur_pass, context);
diff --git a/src/backend/commands/view.c b/src/backend/commands/view.c
index fdad833832..76532aa35d 100644
--- a/src/backend/commands/view.c
+++ b/src/backend/commands/view.c
@@ -30,8 +30,6 @@
 #include "utils/lsyscache.h"
 #include "utils/rel.h"
 
-static void checkViewColumns(TupleDesc newdesc, TupleDesc olddesc);
-
 /*---------------------------------------------------------------------
  * DefineVirtualRelation
  *
@@ -130,7 +128,7 @@ DefineVirtualRelation(RangeVar *relation, List *tlist, bool replace,
 		 * column list.
 		 */
 		descriptor = BuildDescForRelation(attrList);
-		checkViewColumns(descriptor, rel->rd_att);
+		checkViewColumns(descriptor, rel->rd_att, false);
 
 		/*
 		 * If new attributes have been added, we must add pg_attribute entries
@@ -263,15 +261,22 @@ DefineVirtualRelation(RangeVar *relation, List *tlist, bool replace,
  * added to generate specific complaints.  Also, we allow the new view to have
  * more columns than the old.
  */
-static void
-checkViewColumns(TupleDesc newdesc, TupleDesc olddesc)
+void
+checkViewColumns(TupleDesc newdesc, TupleDesc olddesc, bool is_matview)
 {
 	int			i;
 
 	if (newdesc->natts < olddesc->natts)
-		ereport(ERROR,
-				(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
-				 errmsg("cannot drop columns from view")));
+	{
+		if (is_matview)
+			ereport(ERROR,
+					errcode(ERRCODE_INVALID_TABLE_DEFINITION),
+					errmsg("cannot drop columns from materialized view"));
+		else
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
+					 errmsg("cannot drop columns from view")));
+	}
 
 	for (i = 0; i < olddesc->natts; i++)
 	{
@@ -280,17 +285,34 @@ checkViewColumns(TupleDesc newdesc, TupleDesc olddesc)
 
 		/* XXX msg not right, but we don't support DROP COL on view anyway */
 		if (newattr->attisdropped != oldattr->attisdropped)
-			ereport(ERROR,
-					(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
-					 errmsg("cannot drop columns from view")));
+		{
+			if (is_matview)
+				ereport(ERROR,
+						errcode(ERRCODE_INVALID_TABLE_DEFINITION),
+						errmsg("cannot drop columns from materialized view"));
+			else
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
+						 errmsg("cannot drop columns from view")));
+		}
 
 		if (strcmp(NameStr(newattr->attname), NameStr(oldattr->attname)) != 0)
-			ereport(ERROR,
-					(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
-					 errmsg("cannot change name of view column \"%s\" to \"%s\"",
-							NameStr(oldattr->attname),
-							NameStr(newattr->attname)),
-					 errhint("Use ALTER VIEW ... RENAME COLUMN ... to change name of view column instead.")));
+		{
+			if (is_matview)
+				ereport(ERROR,
+						errcode(ERRCODE_INVALID_TABLE_DEFINITION),
+						errmsg("cannot change name of materialized view column \"%s\" to \"%s\"",
+							   NameStr(oldattr->attname),
+							   NameStr(newattr->attname)),
+						errhint("Use ALTER MATERIALIZED VIEW ... RENAME COLUMN ... to change name of materialized view column instead."));
+			else
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
+						 errmsg("cannot change name of view column \"%s\" to \"%s\"",
+								NameStr(oldattr->attname),
+								NameStr(newattr->attname)),
+						 errhint("Use ALTER VIEW ... RENAME COLUMN ... to change name of view column instead.")));
+		}
 
 		/*
 		 * We cannot allow type, typmod, or collation to change, since these
@@ -299,26 +321,48 @@ checkViewColumns(TupleDesc newdesc, TupleDesc olddesc)
 		 */
 		if (newattr->atttypid != oldattr->atttypid ||
 			newattr->atttypmod != oldattr->atttypmod)
-			ereport(ERROR,
-					(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
-					 errmsg("cannot change data type of view column \"%s\" from %s to %s",
-							NameStr(oldattr->attname),
-							format_type_with_typemod(oldattr->atttypid,
-													 oldattr->atttypmod),
-							format_type_with_typemod(newattr->atttypid,
-													 newattr->atttypmod))));
+		{
+			if (is_matview)
+				ereport(ERROR,
+						errcode(ERRCODE_INVALID_TABLE_DEFINITION),
+						errmsg("cannot change data type of materialized view column \"%s\" from %s to %s",
+							   NameStr(oldattr->attname),
+							   format_type_with_typemod(oldattr->atttypid,
+														oldattr->atttypmod),
+							   format_type_with_typemod(newattr->atttypid,
+														newattr->atttypmod)));
+			else
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
+						 errmsg("cannot change data type of view column \"%s\" from %s to %s",
+								NameStr(oldattr->attname),
+								format_type_with_typemod(oldattr->atttypid,
+														 oldattr->atttypmod),
+								format_type_with_typemod(newattr->atttypid,
+														 newattr->atttypmod))));
+		}
 
 		/*
 		 * At this point, attcollations should be both valid or both invalid,
 		 * so applying get_collation_name unconditionally should be fine.
 		 */
 		if (newattr->attcollation != oldattr->attcollation)
-			ereport(ERROR,
-					(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
-					 errmsg("cannot change collation of view column \"%s\" from \"%s\" to \"%s\"",
-							NameStr(oldattr->attname),
-							get_collation_name(oldattr->attcollation),
-							get_collation_name(newattr->attcollation))));
+		{
+			if (is_matview)
+				ereport(ERROR,
+						errcode(ERRCODE_INVALID_TABLE_DEFINITION),
+						errmsg("cannot change collation of materialized view column \"%s\" from \"%s\" to \"%s\"",
+							   NameStr(oldattr->attname),
+							   get_collation_name(oldattr->attcollation),
+							   get_collation_name(newattr->attcollation)));
+			else
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
+						 errmsg("cannot change collation of view column \"%s\" from \"%s\" to \"%s\"",
+								NameStr(oldattr->attname),
+								get_collation_name(oldattr->attcollation),
+								get_collation_name(newattr->attcollation))));
+		}
 	}
 
 	/*
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 84cef57a70..b02b2f42d2 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -4779,6 +4779,21 @@ CreateMatViewStmt:
 					$8->skipData = !($11);
 					$$ = (Node *) ctas;
 				}
+		| CREATE OR REPLACE OptNoLog MATERIALIZED VIEW create_mv_target AS SelectStmt opt_with_data
+				{
+					CreateTableAsStmt *ctas = makeNode(CreateTableAsStmt);
+
+					ctas->query = $9;
+					ctas->into = $7;
+					ctas->objtype = OBJECT_MATVIEW;
+					ctas->is_select_into = false;
+					ctas->if_not_exists = false;
+					/* cram additional flags into the IntoClause */
+					$7->rel->relpersistence = $4;
+					$7->skipData = !($10);
+					$7->replace = true;
+					$$ = (Node *) ctas;
+				}
 		;
 
 create_mv_target:
diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index a7ccde6d7d..c50a3781db 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -1811,7 +1811,7 @@ psql_completion(const char *text, int start, int end)
 	/* complete with something you can create or replace */
 	else if (TailMatches("CREATE", "OR", "REPLACE"))
 		COMPLETE_WITH("FUNCTION", "PROCEDURE", "LANGUAGE", "RULE", "VIEW",
-					  "AGGREGATE", "TRANSFORM", "TRIGGER");
+					  "AGGREGATE", "TRANSFORM", "TRIGGER", "MATERIALIZED VIEW");
 
 /* DROP, but not DROP embedded in other commands */
 	/* complete with something you can drop */
@@ -3599,13 +3599,16 @@ psql_completion(const char *text, int start, int end)
 		COMPLETE_WITH("SELECT");
 
 /* CREATE MATERIALIZED VIEW */
-	else if (Matches("CREATE", "MATERIALIZED"))
+	else if (Matches("CREATE", "MATERIALIZED") ||
+			 Matches("CREATE", "OR", "REPLACE", "MATERIALIZED"))
 		COMPLETE_WITH("VIEW");
-	/* Complete CREATE MATERIALIZED VIEW <name> with AS */
-	else if (Matches("CREATE", "MATERIALIZED", "VIEW", MatchAny))
+	/* Complete CREATE [ OR REPLACE ] MATERIALIZED VIEW <name> with AS */
+	else if (Matches("CREATE", "MATERIALIZED", "VIEW", MatchAny) ||
+			 Matches("CREATE", "OR", "REPLACE", "MATERIALIZED", "VIEW", MatchAny))
 		COMPLETE_WITH("AS");
-	/* Complete "CREATE MATERIALIZED VIEW <sth> AS with "SELECT" */
-	else if (Matches("CREATE", "MATERIALIZED", "VIEW", MatchAny, "AS"))
+	/* Complete "CREATE [ OR REPLACE ] MATERIALIZED VIEW <sth> AS with "SELECT" */
+	else if (Matches("CREATE", "MATERIALIZED", "VIEW", MatchAny, "AS") ||
+			 Matches("CREATE", "OR", "REPLACE", "MATERIALIZED", "VIEW", MatchAny, "AS"))
 		COMPLETE_WITH("SELECT");
 
 /* CREATE EVENT TRIGGER */
diff --git a/src/include/commands/view.h b/src/include/commands/view.h
index d2d8588989..7eacdaaceb 100644
--- a/src/include/commands/view.h
+++ b/src/include/commands/view.h
@@ -22,4 +22,7 @@ extern ObjectAddress DefineView(ViewStmt *stmt, const char *queryString,
 
 extern void StoreViewQuery(Oid viewOid, Query *viewParse, bool replace);
 
+extern void checkViewColumns(TupleDesc newdesc, TupleDesc olddesc,
+							 bool is_matview);
+
 #endif							/* VIEW_H */
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 124d853e49..3ba536eee3 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -2338,7 +2338,7 @@ typedef struct AlterTableStmt
 typedef enum AlterTableType
 {
 	AT_AddColumn,				/* add column */
-	AT_AddColumnToView,			/* implicitly via CREATE OR REPLACE VIEW */
+	AT_AddColumnToView,			/* implicitly via CREATE OR REPLACE [MATERIALIZED] VIEW */
 	AT_ColumnDefault,			/* alter column default */
 	AT_CookedColumnDefault,		/* add a pre-cooked column default */
 	AT_DropNotNull,				/* alter column drop not null */
diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h
index ea47652adb..4b0ee5d10d 100644
--- a/src/include/nodes/primnodes.h
+++ b/src/include/nodes/primnodes.h
@@ -168,6 +168,7 @@ typedef struct IntoClause
 	/* materialized view's SELECT query */
 	Node	   *viewQuery pg_node_attr(query_jumble_ignore);
 	bool		skipData;		/* true for WITH NO DATA */
+	bool		replace;		/* replace existing matview? */
 } IntoClause;
 
 
diff --git a/src/test/regress/expected/matview.out b/src/test/regress/expected/matview.out
index 038ab73517..e2e2a13396 100644
--- a/src/test/regress/expected/matview.out
+++ b/src/test/regress/expected/matview.out
@@ -694,3 +694,194 @@ NOTICE:  relation "matview_ine_tab" already exists, skipping
 (0 rows)
 
 DROP MATERIALIZED VIEW matview_ine_tab;
+--
+-- test CREATE OR REPLACE MATERIALIZED VIEW
+--
+-- matview does not already exist
+DROP MATERIALIZED VIEW IF EXISTS mvtest_replace;
+NOTICE:  materialized view "mvtest_replace" does not exist, skipping
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 1 AS a;
+SELECT * FROM mvtest_replace;
+ a 
+---
+ 1
+(1 row)
+
+-- replace query with data
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 2 AS a;
+SELECT * FROM mvtest_replace;
+ a 
+---
+ 2
+(1 row)
+
+-- replace query without data
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 3 AS a
+  WITH NO DATA;
+SELECT * FROM mvtest_replace; -- error: not populated
+ERROR:  materialized view "mvtest_replace" has not been populated
+HINT:  Use the REFRESH MATERIALIZED VIEW command.
+REFRESH MATERIALIZED VIEW mvtest_replace;
+SELECT * FROM mvtest_replace;
+ a 
+---
+ 3
+(1 row)
+
+-- add column
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 4 AS a, 1 b;
+SELECT * FROM mvtest_replace;
+ a | b 
+---+---
+ 4 | 1
+(1 row)
+
+-- replace table options
+SELECT m.*, c.relname, c.reloptions, s.spcname, a.amname
+  FROM mvtest_replace m
+    CROSS JOIN pg_class c
+    LEFT JOIN pg_tablespace s ON s.oid = c.reltablespace
+    LEFT JOIN pg_am a ON a.oid = c.relam
+  WHERE c.relname = 'mvtest_replace';
+ a | b |    relname     | reloptions | spcname | amname 
+---+---+----------------+------------+---------+--------
+ 4 | 1 | mvtest_replace |            |         | heap
+(1 row)
+
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace
+  USING heap2
+  WITH (fillfactor = 50)
+  TABLESPACE regress_tblspace
+  AS SELECT 5 AS a, 1 AS b;
+SELECT m.*, c.relname, c.reloptions, s.spcname, a.amname
+  FROM mvtest_replace m
+    CROSS JOIN pg_class c
+    LEFT JOIN pg_tablespace s ON s.oid = c.reltablespace
+    LEFT JOIN pg_am a ON a.oid = c.relam
+  WHERE c.relname = 'mvtest_replace';
+ a | b |    relname     |   reloptions    |     spcname      | amname 
+---+---+----------------+-----------------+------------------+--------
+ 5 | 1 | mvtest_replace | {fillfactor=50} | regress_tblspace | heap2
+(1 row)
+
+-- can replace matview that has a dependent view
+CREATE VIEW mvtest_replace_v AS
+  SELECT * FROM mvtest_replace;
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 6 AS a, 1 AS b;
+SELECT * FROM mvtest_replace, mvtest_replace_v;
+ a | b | a | b 
+---+---+---+---
+ 6 | 1 | 6 | 1
+(1 row)
+
+DROP VIEW mvtest_replace_v;
+-- index gets rebuilt when replacing with data
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 7 AS a, 1 AS b;
+CREATE UNIQUE INDEX ON mvtest_replace (b);
+SELECT * FROM mvtest_replace;
+ a | b 
+---+---
+ 7 | 1
+(1 row)
+
+SET enable_seqscan = off; -- force index scan
+EXPLAIN (COSTS OFF) SELECT * FROM mvtest_replace WHERE b = 1;
+                       QUERY PLAN                        
+---------------------------------------------------------
+ Index Scan using mvtest_replace_b_idx on mvtest_replace
+   Index Cond: (b = 1)
+(2 rows)
+
+SELECT * FROM mvtest_replace WHERE b = 1;
+ a | b 
+---+---
+ 7 | 1
+(1 row)
+
+RESET enable_seqscan;
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 8 AS a, 1 AS b;
+SET enable_seqscan = off; -- force index scan
+EXPLAIN (COSTS OFF) SELECT * FROM mvtest_replace WHERE b = 1;
+                       QUERY PLAN                        
+---------------------------------------------------------
+ Index Scan using mvtest_replace_b_idx on mvtest_replace
+   Index Cond: (b = 1)
+(2 rows)
+
+SELECT * FROM mvtest_replace WHERE b = 1;
+ a | b 
+---+---
+ 8 | 1
+(1 row)
+
+RESET enable_seqscan;
+-- cannot change column data type
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 9 AS a, 'x' AS b; -- error
+ERROR:  cannot change data type of materialized view column "b" from integer to text
+SELECT * FROM mvtest_replace;
+ a | b 
+---+---
+ 8 | 1
+(1 row)
+
+-- cannot rename column
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 10 AS a, 1 AS b2; -- error
+ERROR:  cannot change name of materialized view column "b" to "b2"
+HINT:  Use ALTER MATERIALIZED VIEW ... RENAME COLUMN ... to change name of materialized view column instead.
+SELECT * FROM mvtest_replace;
+ a | b 
+---+---
+ 8 | 1
+(1 row)
+
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 11 AS a, 1 AS b, 'y' COLLATE "C" AS c;
+SELECT * FROM mvtest_replace;
+ a  | b | c 
+----+---+---
+ 11 | 1 | y
+(1 row)
+
+-- cannot change column collation
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 12 AS a, 1 AS b, 'x' COLLATE "POSIX" AS c; -- error
+ERROR:  cannot change collation of materialized view column "c" from "C" to "POSIX"
+SELECT * FROM mvtest_replace;
+ a  | b | c 
+----+---+---
+ 11 | 1 | y
+(1 row)
+
+-- cannot drop column
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 13 AS a, 1 AS b; -- error
+ERROR:  cannot drop columns from materialized view
+SELECT * FROM mvtest_replace;
+ a  | b | c 
+----+---+---
+ 11 | 1 | y
+(1 row)
+
+-- must target a matview
+CREATE VIEW mvtest_not_mv AS
+  SELECT 1 AS a;
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_not_mv AS
+  SELECT 1 AS a; -- error
+ERROR:  "mvtest_not_mv" is not a materialized view
+DROP VIEW mvtest_not_mv;
+-- cannot use OR REPLACE with IF NOT EXISTS
+CREATE OR REPLACE MATERIALIZED VIEW IF NOT EXISTS mvtest_replace AS
+  SELECT 1 AS a;
+ERROR:  syntax error at or near "NOT"
+LINE 1: CREATE OR REPLACE MATERIALIZED VIEW IF NOT EXISTS mvtest_rep...
+                                               ^
+DROP MATERIALIZED VIEW mvtest_replace;
diff --git a/src/test/regress/sql/matview.sql b/src/test/regress/sql/matview.sql
index b74ee305e0..c12f0243c9 100644
--- a/src/test/regress/sql/matview.sql
+++ b/src/test/regress/sql/matview.sql
@@ -314,3 +314,111 @@ EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF)
   CREATE MATERIALIZED VIEW IF NOT EXISTS matview_ine_tab AS
     SELECT 1 / 0 WITH NO DATA; -- ok
 DROP MATERIALIZED VIEW matview_ine_tab;
+
+--
+-- test CREATE OR REPLACE MATERIALIZED VIEW
+--
+
+-- matview does not already exist
+DROP MATERIALIZED VIEW IF EXISTS mvtest_replace;
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 1 AS a;
+SELECT * FROM mvtest_replace;
+
+-- replace query with data
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 2 AS a;
+SELECT * FROM mvtest_replace;
+
+-- replace query without data
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 3 AS a
+  WITH NO DATA;
+SELECT * FROM mvtest_replace; -- error: not populated
+REFRESH MATERIALIZED VIEW mvtest_replace;
+SELECT * FROM mvtest_replace;
+
+-- add column
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 4 AS a, 1 b;
+SELECT * FROM mvtest_replace;
+
+-- replace table options
+SELECT m.*, c.relname, c.reloptions, s.spcname, a.amname
+  FROM mvtest_replace m
+    CROSS JOIN pg_class c
+    LEFT JOIN pg_tablespace s ON s.oid = c.reltablespace
+    LEFT JOIN pg_am a ON a.oid = c.relam
+  WHERE c.relname = 'mvtest_replace';
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace
+  USING heap2
+  WITH (fillfactor = 50)
+  TABLESPACE regress_tblspace
+  AS SELECT 5 AS a, 1 AS b;
+SELECT m.*, c.relname, c.reloptions, s.spcname, a.amname
+  FROM mvtest_replace m
+    CROSS JOIN pg_class c
+    LEFT JOIN pg_tablespace s ON s.oid = c.reltablespace
+    LEFT JOIN pg_am a ON a.oid = c.relam
+  WHERE c.relname = 'mvtest_replace';
+
+-- can replace matview that has a dependent view
+CREATE VIEW mvtest_replace_v AS
+  SELECT * FROM mvtest_replace;
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 6 AS a, 1 AS b;
+SELECT * FROM mvtest_replace, mvtest_replace_v;
+DROP VIEW mvtest_replace_v;
+
+-- index gets rebuilt when replacing with data
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 7 AS a, 1 AS b;
+CREATE UNIQUE INDEX ON mvtest_replace (b);
+SELECT * FROM mvtest_replace;
+SET enable_seqscan = off; -- force index scan
+EXPLAIN (COSTS OFF) SELECT * FROM mvtest_replace WHERE b = 1;
+SELECT * FROM mvtest_replace WHERE b = 1;
+RESET enable_seqscan;
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 8 AS a, 1 AS b;
+SET enable_seqscan = off; -- force index scan
+EXPLAIN (COSTS OFF) SELECT * FROM mvtest_replace WHERE b = 1;
+SELECT * FROM mvtest_replace WHERE b = 1;
+RESET enable_seqscan;
+
+-- cannot change column data type
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 9 AS a, 'x' AS b; -- error
+SELECT * FROM mvtest_replace;
+
+-- cannot rename column
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 10 AS a, 1 AS b2; -- error
+SELECT * FROM mvtest_replace;
+
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 11 AS a, 1 AS b, 'y' COLLATE "C" AS c;
+SELECT * FROM mvtest_replace;
+
+-- cannot change column collation
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 12 AS a, 1 AS b, 'x' COLLATE "POSIX" AS c; -- error
+SELECT * FROM mvtest_replace;
+
+-- cannot drop column
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 13 AS a, 1 AS b; -- error
+SELECT * FROM mvtest_replace;
+
+-- must target a matview
+CREATE VIEW mvtest_not_mv AS
+  SELECT 1 AS a;
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_not_mv AS
+  SELECT 1 AS a; -- error
+DROP VIEW mvtest_not_mv;
+
+-- cannot use OR REPLACE with IF NOT EXISTS
+CREATE OR REPLACE MATERIALIZED VIEW IF NOT EXISTS mvtest_replace AS
+  SELECT 1 AS a;
+
+DROP MATERIALIZED VIEW mvtest_replace;
-- 
2.46.0

  [text/x-diff] v3-0002-Deprecate-CREATE-MATERIALIZED-VIEW-IF-NOT-EXISTS.patch (8.5K, ../../b74736cf-a4b4-4c32-8df1-d08abe9a14ed@ewie.name/3-v3-0002-Deprecate-CREATE-MATERIALIZED-VIEW-IF-NOT-EXISTS.patch)
  download | inline diff:
From 28c5700bae846c2ce3cb0ca552f5dca256420a9e Mon Sep 17 00:00:00 2001
From: Erik Wienhold <ewie@ewie.name>
Date: Tue, 28 May 2024 02:19:53 +0200
Subject: [PATCH v3 2/3] Deprecate CREATE MATERIALIZED VIEW IF NOT EXISTS

---
 src/backend/parser/gram.y                     | 14 +++++++++++++
 .../expected/test_extensions.out              | 18 +++++++++++++++++
 src/test/regress/expected/matview.out         | 20 +++++++++++++++++++
 3 files changed, 52 insertions(+)

diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index b02b2f42d2..4b939ea7ca 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -4778,6 +4778,20 @@ CreateMatViewStmt:
 					$8->rel->relpersistence = $2;
 					$8->skipData = !($11);
 					$$ = (Node *) ctas;
+
+					if (ctas->into->rel->schemaname)
+							ereport(WARNING,
+									errmsg("IF NOT EXISTS is deprecated in materialized view creation"),
+									errhint("Use CREATE OR REPLACE MATERIALIZED VIEW %s.%s.",
+											ctas->into->rel->schemaname,
+											ctas->into->rel->relname),
+									parser_errposition(@1));
+					else
+							ereport(WARNING,
+									errmsg("IF NOT EXISTS is deprecated in materialized view creation"),
+									errhint("Use CREATE OR REPLACE MATERIALIZED VIEW %s.",
+											ctas->into->rel->relname),
+									parser_errposition(@1));
 				}
 		| CREATE OR REPLACE OptNoLog MATERIALIZED VIEW create_mv_target AS SelectStmt opt_with_data
 				{
diff --git a/src/test/modules/test_extensions/expected/test_extensions.out b/src/test/modules/test_extensions/expected/test_extensions.out
index f357cc21aa..ecd453b29d 100644
--- a/src/test/modules/test_extensions/expected/test_extensions.out
+++ b/src/test/modules/test_extensions/expected/test_extensions.out
@@ -377,41 +377,57 @@ Objects in extension "test_ext_cor"
 CREATE COLLATION ext_cine_coll
   ( LC_COLLATE = "C", LC_CTYPE = "C" );
 CREATE EXTENSION test_ext_cine;  -- fail
+WARNING:  IF NOT EXISTS is deprecated in materialized view creation
+HINT:  Use CREATE OR REPLACE MATERIALIZED VIEW ext_cine_mv.
 ERROR:  collation ext_cine_coll is not a member of extension "test_ext_cine"
 DETAIL:  An extension may only use CREATE ... IF NOT EXISTS to skip object creation if the conflicting object is one that it already owns.
 DROP COLLATION ext_cine_coll;
 CREATE MATERIALIZED VIEW ext_cine_mv AS SELECT 11 AS f1;
 CREATE EXTENSION test_ext_cine;  -- fail
+WARNING:  IF NOT EXISTS is deprecated in materialized view creation
+HINT:  Use CREATE OR REPLACE MATERIALIZED VIEW ext_cine_mv.
 ERROR:  materialized view ext_cine_mv is not a member of extension "test_ext_cine"
 DETAIL:  An extension may only use CREATE ... IF NOT EXISTS to skip object creation if the conflicting object is one that it already owns.
 DROP MATERIALIZED VIEW ext_cine_mv;
 CREATE FOREIGN DATA WRAPPER dummy;
 CREATE SERVER ext_cine_srv FOREIGN DATA WRAPPER dummy;
 CREATE EXTENSION test_ext_cine;  -- fail
+WARNING:  IF NOT EXISTS is deprecated in materialized view creation
+HINT:  Use CREATE OR REPLACE MATERIALIZED VIEW ext_cine_mv.
 ERROR:  server ext_cine_srv is not a member of extension "test_ext_cine"
 DETAIL:  An extension may only use CREATE ... IF NOT EXISTS to skip object creation if the conflicting object is one that it already owns.
 DROP SERVER ext_cine_srv;
 CREATE SCHEMA ext_cine_schema;
 CREATE EXTENSION test_ext_cine;  -- fail
+WARNING:  IF NOT EXISTS is deprecated in materialized view creation
+HINT:  Use CREATE OR REPLACE MATERIALIZED VIEW ext_cine_mv.
 ERROR:  schema ext_cine_schema is not a member of extension "test_ext_cine"
 DETAIL:  An extension may only use CREATE ... IF NOT EXISTS to skip object creation if the conflicting object is one that it already owns.
 DROP SCHEMA ext_cine_schema;
 CREATE SEQUENCE ext_cine_seq;
 CREATE EXTENSION test_ext_cine;  -- fail
+WARNING:  IF NOT EXISTS is deprecated in materialized view creation
+HINT:  Use CREATE OR REPLACE MATERIALIZED VIEW ext_cine_mv.
 ERROR:  sequence ext_cine_seq is not a member of extension "test_ext_cine"
 DETAIL:  An extension may only use CREATE ... IF NOT EXISTS to skip object creation if the conflicting object is one that it already owns.
 DROP SEQUENCE ext_cine_seq;
 CREATE TABLE ext_cine_tab1 (x int);
 CREATE EXTENSION test_ext_cine;  -- fail
+WARNING:  IF NOT EXISTS is deprecated in materialized view creation
+HINT:  Use CREATE OR REPLACE MATERIALIZED VIEW ext_cine_mv.
 ERROR:  table ext_cine_tab1 is not a member of extension "test_ext_cine"
 DETAIL:  An extension may only use CREATE ... IF NOT EXISTS to skip object creation if the conflicting object is one that it already owns.
 DROP TABLE ext_cine_tab1;
 CREATE TABLE ext_cine_tab2 AS SELECT 42 AS y;
 CREATE EXTENSION test_ext_cine;  -- fail
+WARNING:  IF NOT EXISTS is deprecated in materialized view creation
+HINT:  Use CREATE OR REPLACE MATERIALIZED VIEW ext_cine_mv.
 ERROR:  table ext_cine_tab2 is not a member of extension "test_ext_cine"
 DETAIL:  An extension may only use CREATE ... IF NOT EXISTS to skip object creation if the conflicting object is one that it already owns.
 DROP TABLE ext_cine_tab2;
 CREATE EXTENSION test_ext_cine;
+WARNING:  IF NOT EXISTS is deprecated in materialized view creation
+HINT:  Use CREATE OR REPLACE MATERIALIZED VIEW ext_cine_mv.
 \dx+ test_ext_cine
 Objects in extension "test_ext_cine"
         Object description         
@@ -433,6 +449,8 @@ Objects in extension "test_ext_cine"
 (14 rows)
 
 ALTER EXTENSION test_ext_cine UPDATE TO '1.1';
+WARNING:  IF NOT EXISTS is deprecated in materialized view creation
+HINT:  Use CREATE OR REPLACE MATERIALIZED VIEW ext_cine_mv.
 \dx+ test_ext_cine
 Objects in extension "test_ext_cine"
         Object description         
diff --git a/src/test/regress/expected/matview.out b/src/test/regress/expected/matview.out
index e2e2a13396..cefd0d442c 100644
--- a/src/test/regress/expected/matview.out
+++ b/src/test/regress/expected/matview.out
@@ -565,6 +565,10 @@ CREATE MATERIALIZED VIEW mvtest_mv_foo AS SELECT * FROM mvtest_foo_data;
 CREATE MATERIALIZED VIEW mvtest_mv_foo AS SELECT * FROM mvtest_foo_data;
 ERROR:  relation "mvtest_mv_foo" already exists
 CREATE MATERIALIZED VIEW IF NOT EXISTS mvtest_mv_foo AS SELECT * FROM mvtest_foo_data;
+WARNING:  IF NOT EXISTS is deprecated in materialized view creation
+LINE 1: CREATE MATERIALIZED VIEW IF NOT EXISTS mvtest_mv_foo AS SELE...
+        ^
+HINT:  Use CREATE OR REPLACE MATERIALIZED VIEW mvtest_mv_foo.
 NOTICE:  relation "mvtest_mv_foo" already exists, skipping
 CREATE UNIQUE INDEX ON mvtest_mv_foo (i);
 RESET ROLE;
@@ -662,12 +666,20 @@ CREATE MATERIALIZED VIEW matview_ine_tab AS SELECT 1 / 0; -- error
 ERROR:  relation "matview_ine_tab" already exists
 CREATE MATERIALIZED VIEW IF NOT EXISTS matview_ine_tab AS
   SELECT 1 / 0; -- ok
+WARNING:  IF NOT EXISTS is deprecated in materialized view creation
+LINE 1: CREATE MATERIALIZED VIEW IF NOT EXISTS matview_ine_tab AS
+        ^
+HINT:  Use CREATE OR REPLACE MATERIALIZED VIEW matview_ine_tab.
 NOTICE:  relation "matview_ine_tab" already exists, skipping
 CREATE MATERIALIZED VIEW matview_ine_tab AS
   SELECT 1 / 0 WITH NO DATA; -- error
 ERROR:  relation "matview_ine_tab" already exists
 CREATE MATERIALIZED VIEW IF NOT EXISTS matview_ine_tab AS
   SELECT 1 / 0 WITH NO DATA; -- ok
+WARNING:  IF NOT EXISTS is deprecated in materialized view creation
+LINE 1: CREATE MATERIALIZED VIEW IF NOT EXISTS matview_ine_tab AS
+        ^
+HINT:  Use CREATE OR REPLACE MATERIALIZED VIEW matview_ine_tab.
 NOTICE:  relation "matview_ine_tab" already exists, skipping
 EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF)
   CREATE MATERIALIZED VIEW matview_ine_tab AS
@@ -676,6 +688,10 @@ ERROR:  relation "matview_ine_tab" already exists
 EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF)
   CREATE MATERIALIZED VIEW IF NOT EXISTS matview_ine_tab AS
     SELECT 1 / 0; -- ok
+WARNING:  IF NOT EXISTS is deprecated in materialized view creation
+LINE 2:   CREATE MATERIALIZED VIEW IF NOT EXISTS matview_ine_tab AS
+          ^
+HINT:  Use CREATE OR REPLACE MATERIALIZED VIEW matview_ine_tab.
 NOTICE:  relation "matview_ine_tab" already exists, skipping
  QUERY PLAN 
 ------------
@@ -688,6 +704,10 @@ ERROR:  relation "matview_ine_tab" already exists
 EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF)
   CREATE MATERIALIZED VIEW IF NOT EXISTS matview_ine_tab AS
     SELECT 1 / 0 WITH NO DATA; -- ok
+WARNING:  IF NOT EXISTS is deprecated in materialized view creation
+LINE 2:   CREATE MATERIALIZED VIEW IF NOT EXISTS matview_ine_tab AS
+          ^
+HINT:  Use CREATE OR REPLACE MATERIALIZED VIEW matview_ine_tab.
 NOTICE:  relation "matview_ine_tab" already exists, skipping
  QUERY PLAN 
 ------------
-- 
2.46.0

  [text/x-diff] v3-0003-Replace-matview-WITH-OLD-DATA.patch (7.7K, ../../b74736cf-a4b4-4c32-8df1-d08abe9a14ed@ewie.name/4-v3-0003-Replace-matview-WITH-OLD-DATA.patch)
  download | inline diff:
From db9be5bcfb8a8f2e63386004a68b7b026d56e9df Mon Sep 17 00:00:00 2001
From: Erik Wienhold <ewie@ewie.name>
Date: Fri, 26 Jul 2024 23:33:15 +0200
Subject: [PATCH v3 3/3] Replace matview WITH OLD DATA

---
 .../sgml/ref/create_materialized_view.sgml    | 16 +++++++++--
 src/backend/commands/createas.c               | 26 +++++++++++------
 src/backend/parser/gram.y                     | 16 +++++++++++
 src/include/nodes/primnodes.h                 |  1 +
 src/test/regress/expected/matview.out         | 28 +++++++++++++++++++
 src/test/regress/sql/matview.sql              | 15 ++++++++++
 6 files changed, 90 insertions(+), 12 deletions(-)

diff --git a/doc/src/sgml/ref/create_materialized_view.sgml b/doc/src/sgml/ref/create_materialized_view.sgml
index b5a8e3441a..65633b8bfa 100644
--- a/doc/src/sgml/ref/create_materialized_view.sgml
+++ b/doc/src/sgml/ref/create_materialized_view.sgml
@@ -27,7 +27,7 @@ CREATE [ OR REPLACE ] MATERIALIZED VIEW [ IF NOT EXISTS ] <replaceable>table_nam
     [ WITH ( <replaceable class="parameter">storage_parameter</replaceable> [= <replaceable class="parameter">value</replaceable>] [, ... ] ) ]
     [ TABLESPACE <replaceable class="parameter">tablespace_name</replaceable> ]
     AS <replaceable>query</replaceable>
-    [ WITH [ NO ] DATA ]
+    [ WITH [ NO | OLD ] DATA ]
 </synopsis>
  </refsynopsisdiv>
 
@@ -37,7 +37,8 @@ CREATE [ OR REPLACE ] MATERIALIZED VIEW [ IF NOT EXISTS ] <replaceable>table_nam
   <para>
    <command>CREATE MATERIALIZED VIEW</command> defines a materialized view of
    a query.  The query is executed and used to populate the view at the time
-   the command is issued (unless <command>WITH NO DATA</command> is used) and may be
+   the command is issued (unless <command>WITH NO DATA</command> or
+   <command>WITH OLD DATA</command> is used) and may be
    refreshed later using <command>REFRESH MATERIALIZED VIEW</command>.
   </para>
 
@@ -160,7 +161,7 @@ CREATE [ OR REPLACE ] MATERIALIZED VIEW [ IF NOT EXISTS ] <replaceable>table_nam
    </varlistentry>
 
    <varlistentry>
-    <term><literal>WITH [ NO ] DATA</literal></term>
+    <term><literal>WITH [ NO | OLD ] DATA</literal></term>
     <listitem>
      <para>
       This clause specifies whether or not the materialized view should be
@@ -168,6 +169,15 @@ CREATE [ OR REPLACE ] MATERIALIZED VIEW [ IF NOT EXISTS ] <replaceable>table_nam
       flagged as unscannable and cannot be queried until <command>REFRESH
       MATERIALIZED VIEW</command> is used.
      </para>
+
+     <para>
+      The form <command>WITH OLD DATA</command> keeps the already stored data
+      when replacing an existing materialized view to keep it populated.  For
+      newly created materialized views, this has the same effect as
+      <command>WITH DATA</command>.  Use this form if you want to use
+      <command>REFRESH MATERIALIZED VIEW CONCURRENTLY</command> as it requires
+      a populated materialized view.
+     </para>
     </listitem>
    </varlistentry>
 
diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c
index e4ed3748f9..96e7b81966 100644
--- a/src/backend/commands/createas.c
+++ b/src/backend/commands/createas.c
@@ -331,18 +331,26 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt,
 		/* An existing materialized view can be replaced. */
 		if (is_matview && into->replace)
 		{
-			RefreshMatViewStmt *refresh;
-
 			/* Change the relation to match the new query and other options. */
-			(void) create_ctas_nodata(query->targetList, into);
+			address = create_ctas_nodata(query->targetList, into);
 
-			/* Refresh the materialized view with a fake statement. */
-			refresh = makeNode(RefreshMatViewStmt);
-			refresh->relation = into->rel;
-			refresh->skipData = into->skipData;
-			refresh->concurrent = false;
+			/*
+			 * Refresh the materialized view with a fake statement unless we
+			 * must keep the old data.
+			 */
+			if (!into->keepData)
+			{
+				RefreshMatViewStmt *refresh;
+
+				refresh = makeNode(RefreshMatViewStmt);
+				refresh->relation = into->rel;
+				refresh->skipData = into->skipData;
+				refresh->concurrent = false;
+
+				address = ExecRefreshMatView(refresh, NULL, NULL);
+			}
 
-			return ExecRefreshMatView(refresh, NULL, NULL);
+			return address;
 		}
 
 		return InvalidObjectAddress;
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 4b939ea7ca..e45ebb24f8 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -4808,6 +4808,22 @@ CreateMatViewStmt:
 					$7->replace = true;
 					$$ = (Node *) ctas;
 				}
+		| CREATE OR REPLACE OptNoLog MATERIALIZED VIEW create_mv_target AS SelectStmt WITH OLD DATA_P
+				{
+					CreateTableAsStmt *ctas = makeNode(CreateTableAsStmt);
+
+					ctas->query = $9;
+					ctas->into = $7;
+					ctas->objtype = OBJECT_MATVIEW;
+					ctas->is_select_into = false;
+					ctas->if_not_exists = false;
+					/* cram additional flags into the IntoClause */
+					$7->rel->relpersistence = $4;
+					$7->skipData = false;
+					$7->keepData = true;
+					$7->replace = true;
+					$$ = (Node *) ctas;
+				}
 		;
 
 create_mv_target:
diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h
index 4b0ee5d10d..ae84cb522e 100644
--- a/src/include/nodes/primnodes.h
+++ b/src/include/nodes/primnodes.h
@@ -168,6 +168,7 @@ typedef struct IntoClause
 	/* materialized view's SELECT query */
 	Node	   *viewQuery pg_node_attr(query_jumble_ignore);
 	bool		skipData;		/* true for WITH NO DATA */
+	bool		keepData;		/* true for WITH OLD DATA */
 	bool		replace;		/* replace existing matview? */
 } IntoClause;
 
diff --git a/src/test/regress/expected/matview.out b/src/test/regress/expected/matview.out
index cefd0d442c..47dfd88bff 100644
--- a/src/test/regress/expected/matview.out
+++ b/src/test/regress/expected/matview.out
@@ -751,6 +751,23 @@ SELECT * FROM mvtest_replace;
  3
 (1 row)
 
+-- replace query but keep old data
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 5 AS a
+  WITH OLD DATA;
+SELECT * FROM mvtest_replace;
+ a 
+---
+ 3
+(1 row)
+
+REFRESH MATERIALIZED VIEW mvtest_replace;
+SELECT * FROM mvtest_replace;
+ a 
+---
+ 5
+(1 row)
+
 -- add column
 CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
   SELECT 4 AS a, 1 b;
@@ -905,3 +922,14 @@ ERROR:  syntax error at or near "NOT"
 LINE 1: CREATE OR REPLACE MATERIALIZED VIEW IF NOT EXISTS mvtest_rep...
                                                ^
 DROP MATERIALIZED VIEW mvtest_replace;
+-- Create new matview WITH OLD DATA.  This populates the new matview as if
+-- WITH DATA had been specified.
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 17 AS a
+  WITH OLD DATA;
+SELECT * FROM mvtest_replace;
+ a  
+----
+ 17
+(1 row)
+
diff --git a/src/test/regress/sql/matview.sql b/src/test/regress/sql/matview.sql
index c12f0243c9..b268237c24 100644
--- a/src/test/regress/sql/matview.sql
+++ b/src/test/regress/sql/matview.sql
@@ -338,6 +338,14 @@ SELECT * FROM mvtest_replace; -- error: not populated
 REFRESH MATERIALIZED VIEW mvtest_replace;
 SELECT * FROM mvtest_replace;
 
+-- replace query but keep old data
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 5 AS a
+  WITH OLD DATA;
+SELECT * FROM mvtest_replace;
+REFRESH MATERIALIZED VIEW mvtest_replace;
+SELECT * FROM mvtest_replace;
+
 -- add column
 CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
   SELECT 4 AS a, 1 b;
@@ -422,3 +430,10 @@ CREATE OR REPLACE MATERIALIZED VIEW IF NOT EXISTS mvtest_replace AS
   SELECT 1 AS a;
 
 DROP MATERIALIZED VIEW mvtest_replace;
+
+-- Create new matview WITH OLD DATA.  This populates the new matview as if
+-- WITH DATA had been specified.
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 17 AS a
+  WITH OLD DATA;
+SELECT * FROM mvtest_replace;
-- 
2.46.0

^ permalink  raw  reply  [nested|flat] 29+ messages in thread

* Re: CREATE OR REPLACE MATERIALIZED VIEW
@ 2024-10-31 00:22  Erik Wienhold <ewie@ewie.name>
  parent: Erik Wienhold <ewie@ewie.name>
  0 siblings, 1 reply; 29+ messages in thread

From: Erik Wienhold @ 2024-10-31 00:22 UTC (permalink / raw)
  To: Said Assemlal <sassemlal@neurorx.com>; +Cc: pgsql-hackers

On 2024-09-05 22:33 +0200, Erik Wienhold wrote:
> On 2024-07-27 02:45 +0200, Erik Wienhold wrote:
> > On 2024-07-12 16:49 +0200, Said Assemlal wrote:
> > > > My initial idea, while writing the patch, was that one could replace the
> > > > matview without populating it and then run the concurrent refresh, like
> > > > this:
> > > > 
> > > >      CREATE OR REPLACE MATERIALIZED VIEW foo AS ... WITH NO DATA;
> > > >      REFRESH MATERIALIZED VIEW CONCURRENTLY foo;
> > > > 
> > > > But that won't work because concurrent refresh requires an already
> > > > populated matview.
> > > > 
> > > > Right now the patch either populates the replaced matview or leaves it
> > > > in an unscannable state.  Technically, it's also possible to skip the
> > > > refresh and leave the old data in place, perhaps by specifying
> > > > WITH *OLD* DATA.  New columns would just be null.  Of course you can't
> > > > tell if you got stale data without knowing how the matview was replaced.
> > > > Thoughts?
> > > 
> > > I believe the expectation is to get materialized views updated whenever it
> > > gets replaced so likely to confuse users ?
> > 
> > I agree, that could be confusing -- unless it's well documented.  The
> > attached 0003 implements WITH OLD DATA and states in the docs that this
> > is intended to be used before a concurrent refresh.
> > 
> > Patch 0001 now covers all matview cases in psql's tab completion.  I
> > missed some of them with v1.
> 
> Here's a rebased version due to conflicts with f683d3a4ca and
> 1e35951e71.  No other changes since v2.

rebased

-- 
Erik

Attachments:

  [text/x-diff] v4-0001-Add-CREATE-OR-REPLACE-MATERIALIZED-VIEW.patch (31.8K, ../../d4bf3ccf-5c2b-448d-ad16-59dd6fbe2a2e@ewie.name/2-v4-0001-Add-CREATE-OR-REPLACE-MATERIALIZED-VIEW.patch)
  download | inline diff:
From eefed7938bc1511a1ae998f67c67661c366484ec Mon Sep 17 00:00:00 2001
From: Erik Wienhold <ewie@ewie.name>
Date: Tue, 21 May 2024 18:35:47 +0200
Subject: [PATCH v4 1/3] Add CREATE OR REPLACE MATERIALIZED VIEW

---
 .../sgml/ref/create_materialized_view.sgml    |  15 +-
 src/backend/commands/createas.c               | 207 ++++++++++++++----
 src/backend/commands/tablecmds.c              |   8 +-
 src/backend/commands/view.c                   | 106 ++++++---
 src/backend/parser/gram.y                     |  15 ++
 src/bin/psql/tab-complete.in.c                |  15 +-
 src/include/commands/view.h                   |   3 +
 src/include/nodes/parsenodes.h                |   2 +-
 src/include/nodes/primnodes.h                 |   1 +
 src/test/regress/expected/matview.out         | 191 ++++++++++++++++
 src/test/regress/sql/matview.sql              | 108 +++++++++
 11 files changed, 582 insertions(+), 89 deletions(-)

diff --git a/doc/src/sgml/ref/create_materialized_view.sgml b/doc/src/sgml/ref/create_materialized_view.sgml
index 62d897931c..5e03320eb7 100644
--- a/doc/src/sgml/ref/create_materialized_view.sgml
+++ b/doc/src/sgml/ref/create_materialized_view.sgml
@@ -21,7 +21,7 @@ PostgreSQL documentation
 
  <refsynopsisdiv>
 <synopsis>
-CREATE MATERIALIZED VIEW [ IF NOT EXISTS ] <replaceable>table_name</replaceable>
+CREATE [ OR REPLACE ] MATERIALIZED VIEW [ IF NOT EXISTS ] <replaceable>table_name</replaceable>
     [ (<replaceable>column_name</replaceable> [, ...] ) ]
     [ USING <replaceable class="parameter">method</replaceable> ]
     [ WITH ( <replaceable class="parameter">storage_parameter</replaceable> [= <replaceable class="parameter">value</replaceable>] [, ... ] ) ]
@@ -60,6 +60,17 @@ CREATE MATERIALIZED VIEW [ IF NOT EXISTS ] <replaceable>table_name</replaceable>
   <title>Parameters</title>
 
   <variablelist>
+   <varlistentry>
+    <term><literal>OR REPLACE</literal></term>
+    <listitem>
+     <para>
+      Replaces a materialized view if it already exists.
+      Specifying <literal>OR REPLACE</literal> together with
+      <literal>IF NOT EXISTS</literal> is an error.
+     </para>
+    </listitem>
+   </varlistentry>
+
    <varlistentry>
     <term><literal>IF NOT EXISTS</literal></term>
     <listitem>
@@ -67,7 +78,7 @@ CREATE MATERIALIZED VIEW [ IF NOT EXISTS ] <replaceable>table_name</replaceable>
       Do not throw an error if a materialized view with the same name already
       exists. A notice is issued in this case.  Note that there is no guarantee
       that the existing materialized view is anything like the one that would
-      have been created.
+      have been created, unless you use <literal>OR REPLACE</literal> instead.
      </para>
     </listitem>
    </varlistentry>
diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c
index 5c92e48a56..d4bc5e5c08 100644
--- a/src/backend/commands/createas.c
+++ b/src/backend/commands/createas.c
@@ -79,55 +79,151 @@ static void intorel_destroy(DestReceiver *self);
 static ObjectAddress
 create_ctas_internal(List *attrList, IntoClause *into)
 {
-	CreateStmt *create = makeNode(CreateStmt);
-	bool		is_matview;
+	bool		is_matview,
+				replace = false;
 	char		relkind;
-	Datum		toast_options;
-	const char *const validnsps[] = HEAP_RELOPT_NAMESPACES;
+	Oid			matviewOid = InvalidOid;
 	ObjectAddress intoRelationAddr;
 
 	/* This code supports both CREATE TABLE AS and CREATE MATERIALIZED VIEW */
 	is_matview = (into->viewQuery != NULL);
 	relkind = is_matview ? RELKIND_MATVIEW : RELKIND_RELATION;
 
-	/*
-	 * Create the target relation by faking up a CREATE TABLE parsetree and
-	 * passing it to DefineRelation.
-	 */
-	create->relation = into->rel;
-	create->tableElts = attrList;
-	create->inhRelations = NIL;
-	create->ofTypename = NULL;
-	create->constraints = NIL;
-	create->options = into->options;
-	create->oncommit = into->onCommit;
-	create->tablespacename = into->tableSpaceName;
-	create->if_not_exists = false;
-	create->accessMethod = into->accessMethod;
+	/* Check if an existing materialized view needs to be replaced. */
+	if (is_matview)
+	{
+		LOCKMODE	lockmode;
 
-	/*
-	 * Create the relation.  (This will error out if there's an existing view,
-	 * so we don't need more code to complain if "replace" is false.)
-	 */
-	intoRelationAddr = DefineRelation(create, relkind, InvalidOid, NULL, NULL);
+		lockmode = into->replace ? AccessExclusiveLock : NoLock;
+		(void) RangeVarGetAndCheckCreationNamespace(into->rel, lockmode,
+													&matviewOid);
+		replace = OidIsValid(matviewOid) && into->replace;
+	}
 
-	/*
-	 * If necessary, create a TOAST table for the target table.  Note that
-	 * NewRelationCreateToastTable ends with CommandCounterIncrement(), so
-	 * that the TOAST table will be visible for insertion.
-	 */
-	CommandCounterIncrement();
+	if (is_matview && replace)
+	{
+		Relation	rel;
+		List	   *atcmds = NIL;
+		AlterTableCmd *atcmd;
+		TupleDesc	descriptor;
+
+		rel = relation_open(matviewOid, NoLock);
+
+		if (rel->rd_rel->relkind != RELKIND_MATVIEW)
+			ereport(ERROR,
+					errcode(ERRCODE_WRONG_OBJECT_TYPE),
+					errmsg("\"%s\" is not a materialized view",
+						   RelationGetRelationName(rel)));
+
+		CheckTableNotInUse(rel, "CREATE OR REPLACE MATERIALIZED VIEW");
+
+		descriptor = BuildDescForRelation(attrList);
+		checkViewColumns(descriptor, rel->rd_att, true);
+
+		/* Add new attributes via ALTER TABLE. */
+		if (list_length(attrList) > rel->rd_att->natts)
+		{
+			ListCell   *c;
+			int			skip = rel->rd_att->natts;
+
+			foreach(c, attrList)
+			{
+				if (skip > 0)
+				{
+					skip--;
+					continue;
+				}
+				atcmd = makeNode(AlterTableCmd);
+				atcmd->subtype = AT_AddColumnToView;
+				atcmd->def = (Node *) lfirst(c);
+				atcmds = lappend(atcmds, atcmd);
+			}
+		}
+
+		/* Set access method via ALTER TABLE. */
+		if (into->accessMethod != NULL)
+		{
+			atcmd = makeNode(AlterTableCmd);
+			atcmd->subtype = AT_SetAccessMethod;
+			atcmd->name = into->accessMethod;
+			atcmds = lappend(atcmds, atcmd);
+		}
+
+		/* Set tablespace via ALTER TABLE. */
+		if (into->tableSpaceName != NULL)
+		{
+			atcmd = makeNode(AlterTableCmd);
+			atcmd->subtype = AT_SetTableSpace;
+			atcmd->name = into->tableSpaceName;
+			atcmds = lappend(atcmds, atcmd);
+		}
+
+		/* Set storage parameters via ALTER TABLE. */
+		if (into->options != NIL)
+		{
+			atcmd = makeNode(AlterTableCmd);
+			atcmd->subtype = AT_ReplaceRelOptions;
+			atcmd->def = (Node *) into->options;
+			atcmds = lappend(atcmds, atcmd);
+		}
+
+		if (atcmds != NIL)
+		{
+			AlterTableInternal(matviewOid, atcmds, true);
+			CommandCounterIncrement();
+		}
+
+		relation_close(rel, NoLock);
+
+		ObjectAddressSet(intoRelationAddr, RelationRelationId, matviewOid);
+	}
+	else
+	{
+		CreateStmt *create = makeNode(CreateStmt);
+		Datum		toast_options;
+		const static char *validnsps[] = HEAP_RELOPT_NAMESPACES;
+
+		/*
+		 * Create the target relation by faking up a CREATE TABLE parsetree
+		 * and passing it to DefineRelation.
+		 */
+		create->relation = into->rel;
+		create->tableElts = attrList;
+		create->inhRelations = NIL;
+		create->ofTypename = NULL;
+		create->constraints = NIL;
+		create->options = into->options;
+		create->oncommit = into->onCommit;
+		create->tablespacename = into->tableSpaceName;
+		create->if_not_exists = false;
+		create->accessMethod = into->accessMethod;
+
+		/*
+		 * Create the relation.  (This will error out if there's an existing
+		 * view, so we don't need more code to complain if "replace" is
+		 * false.)
+		 */
+		intoRelationAddr = DefineRelation(create, relkind, InvalidOid, NULL,
+										  NULL);
 
-	/* parse and validate reloptions for the toast table */
-	toast_options = transformRelOptions((Datum) 0,
-										create->options,
-										"toast",
-										validnsps,
-										true, false);
+		/*
+		 * If necessary, create a TOAST table for the target table.  Note that
+		 * NewRelationCreateToastTable ends with CommandCounterIncrement(), so
+		 * that the TOAST table will be visible for insertion.
+		 */
+		CommandCounterIncrement();
+
+		/* parse and validate reloptions for the toast table */
+		toast_options = transformRelOptions((Datum) 0,
+											create->options,
+											"toast",
+											validnsps,
+											true, false);
 
-	(void) heap_reloptions(RELKIND_TOASTVALUE, toast_options, true);
+		(void) heap_reloptions(RELKIND_TOASTVALUE, toast_options, true);
 
-	NewRelationCreateToastTable(intoRelationAddr.objectId, toast_options);
+		NewRelationCreateToastTable(intoRelationAddr.objectId, toast_options);
+	}
 
 	/* Create the "view" part of a materialized view. */
 	if (is_matview)
@@ -135,7 +231,7 @@ create_ctas_internal(List *attrList, IntoClause *into)
 		/* StoreViewQuery scribbles on tree, so make a copy */
 		Query	   *query = copyObject(into->viewQuery);
 
-		StoreViewQuery(intoRelationAddr.objectId, query, false);
+		StoreViewQuery(intoRelationAddr.objectId, query, replace);
 		CommandCounterIncrement();
 	}
 
@@ -232,7 +328,26 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt,
 
 	/* Check if the relation exists or not */
 	if (CreateTableAsRelExists(stmt))
+	{
+		/* An existing materialized view can be replaced. */
+		if (is_matview && into->replace)
+		{
+			RefreshMatViewStmt *refresh;
+
+			/* Change the relation to match the new query and other options. */
+			(void) create_ctas_nodata(query->targetList, into);
+
+			/* Refresh the materialized view with a fake statement. */
+			refresh = makeNode(RefreshMatViewStmt);
+			refresh->relation = into->rel;
+			refresh->skipData = into->skipData;
+			refresh->concurrent = false;
+
+			return ExecRefreshMatView(refresh, NULL, NULL);
+		}
+
 		return InvalidObjectAddress;
+	}
 
 	/*
 	 * Create the tuple receiver object and insert info it will need
@@ -400,14 +515,15 @@ CreateTableAsRelExists(CreateTableAsStmt *ctas)
 	oldrelid = get_relname_relid(into->rel->relname, nspid);
 	if (OidIsValid(oldrelid))
 	{
-		if (!ctas->if_not_exists)
+		if (!ctas->if_not_exists && !into->replace)
 			ereport(ERROR,
 					(errcode(ERRCODE_DUPLICATE_TABLE),
 					 errmsg("relation \"%s\" already exists",
 							into->rel->relname)));
 
 		/*
-		 * The relation exists and IF NOT EXISTS has been specified.
+		 * The relation exists and IF NOT EXISTS or OR REPLACE has been
+		 * specified.
 		 *
 		 * If we are in an extension script, insist that the pre-existing
 		 * object be a member of the extension, to avoid security risks.
@@ -415,11 +531,12 @@ CreateTableAsRelExists(CreateTableAsStmt *ctas)
 		ObjectAddressSet(address, RelationRelationId, oldrelid);
 		checkMembershipInCurrentExtension(&address);
 
-		/* OK to skip */
-		ereport(NOTICE,
-				(errcode(ERRCODE_DUPLICATE_TABLE),
-				 errmsg("relation \"%s\" already exists, skipping",
-						into->rel->relname)));
+		if (ctas->if_not_exists)
+			/* OK to skip */
+			ereport(NOTICE,
+					(errcode(ERRCODE_DUPLICATE_TABLE),
+					 errmsg("relation \"%s\" already exists, skipping",
+							into->rel->relname)));
 		return true;
 	}
 
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 4345b96de5..436232bcf2 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -4516,7 +4516,7 @@ AlterTableGetLockLevel(List *cmds)
 				 * Subcommands that may be visible to concurrent SELECTs
 				 */
 			case AT_DropColumn: /* change visible to SELECT */
-			case AT_AddColumnToView:	/* CREATE VIEW */
+			case AT_AddColumnToView:	/* via CREATE OR REPLACE [MATERIALIZED] VIEW */
 			case AT_DropOids:	/* used to equiv to DropColumn */
 			case AT_EnableAlwaysRule:	/* may change SELECT rules */
 			case AT_EnableReplicaRule:	/* may change SELECT rules */
@@ -4820,8 +4820,8 @@ ATPrepCmd(List **wqueue, Relation rel, AlterTableCmd *cmd,
 			/* Recursion occurs during execution phase */
 			pass = AT_PASS_ADD_COL;
 			break;
-		case AT_AddColumnToView:	/* add column via CREATE OR REPLACE VIEW */
-			ATSimplePermissions(cmd->subtype, rel, ATT_VIEW);
+		case AT_AddColumnToView:	/* via CREATE OR REPLACE [MATERIALIZED] VIEW */
+			ATSimplePermissions(cmd->subtype, rel, ATT_VIEW | ATT_MATVIEW);
 			ATPrepAddColumn(wqueue, rel, recurse, recursing, true, cmd,
 							lockmode, context);
 			/* Recursion occurs during execution phase */
@@ -5252,7 +5252,7 @@ ATExecCmd(List **wqueue, AlteredTableInfo *tab,
 	switch (cmd->subtype)
 	{
 		case AT_AddColumn:		/* ADD COLUMN */
-		case AT_AddColumnToView:	/* add column via CREATE OR REPLACE VIEW */
+		case AT_AddColumnToView:	/* via CREATE OR REPLACE [MATERIALIZED] VIEW */
 			address = ATExecAddColumn(wqueue, tab, rel, &cmd,
 									  cmd->recurse, false,
 									  lockmode, cur_pass, context);
diff --git a/src/backend/commands/view.c b/src/backend/commands/view.c
index 2bd49eb55e..65b9b2e9bb 100644
--- a/src/backend/commands/view.c
+++ b/src/backend/commands/view.c
@@ -30,8 +30,6 @@
 #include "utils/lsyscache.h"
 #include "utils/rel.h"
 
-static void checkViewColumns(TupleDesc newdesc, TupleDesc olddesc);
-
 /*---------------------------------------------------------------------
  * DefineVirtualRelation
  *
@@ -129,7 +127,7 @@ DefineVirtualRelation(RangeVar *relation, List *tlist, bool replace,
 		 * column list.
 		 */
 		descriptor = BuildDescForRelation(attrList);
-		checkViewColumns(descriptor, rel->rd_att);
+		checkViewColumns(descriptor, rel->rd_att, false);
 
 		/*
 		 * If new attributes have been added, we must add pg_attribute entries
@@ -263,15 +261,22 @@ DefineVirtualRelation(RangeVar *relation, List *tlist, bool replace,
  * added to generate specific complaints.  Also, we allow the new view to have
  * more columns than the old.
  */
-static void
-checkViewColumns(TupleDesc newdesc, TupleDesc olddesc)
+void
+checkViewColumns(TupleDesc newdesc, TupleDesc olddesc, bool is_matview)
 {
 	int			i;
 
 	if (newdesc->natts < olddesc->natts)
-		ereport(ERROR,
-				(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
-				 errmsg("cannot drop columns from view")));
+	{
+		if (is_matview)
+			ereport(ERROR,
+					errcode(ERRCODE_INVALID_TABLE_DEFINITION),
+					errmsg("cannot drop columns from materialized view"));
+		else
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
+					 errmsg("cannot drop columns from view")));
+	}
 
 	for (i = 0; i < olddesc->natts; i++)
 	{
@@ -280,17 +285,34 @@ checkViewColumns(TupleDesc newdesc, TupleDesc olddesc)
 
 		/* XXX msg not right, but we don't support DROP COL on view anyway */
 		if (newattr->attisdropped != oldattr->attisdropped)
-			ereport(ERROR,
-					(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
-					 errmsg("cannot drop columns from view")));
+		{
+			if (is_matview)
+				ereport(ERROR,
+						errcode(ERRCODE_INVALID_TABLE_DEFINITION),
+						errmsg("cannot drop columns from materialized view"));
+			else
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
+						 errmsg("cannot drop columns from view")));
+		}
 
 		if (strcmp(NameStr(newattr->attname), NameStr(oldattr->attname)) != 0)
-			ereport(ERROR,
-					(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
-					 errmsg("cannot change name of view column \"%s\" to \"%s\"",
-							NameStr(oldattr->attname),
-							NameStr(newattr->attname)),
-					 errhint("Use ALTER VIEW ... RENAME COLUMN ... to change name of view column instead.")));
+		{
+			if (is_matview)
+				ereport(ERROR,
+						errcode(ERRCODE_INVALID_TABLE_DEFINITION),
+						errmsg("cannot change name of materialized view column \"%s\" to \"%s\"",
+							   NameStr(oldattr->attname),
+							   NameStr(newattr->attname)),
+						errhint("Use ALTER MATERIALIZED VIEW ... RENAME COLUMN ... to change name of materialized view column instead."));
+			else
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
+						 errmsg("cannot change name of view column \"%s\" to \"%s\"",
+								NameStr(oldattr->attname),
+								NameStr(newattr->attname)),
+						 errhint("Use ALTER VIEW ... RENAME COLUMN ... to change name of view column instead.")));
+		}
 
 		/*
 		 * We cannot allow type, typmod, or collation to change, since these
@@ -299,26 +321,48 @@ checkViewColumns(TupleDesc newdesc, TupleDesc olddesc)
 		 */
 		if (newattr->atttypid != oldattr->atttypid ||
 			newattr->atttypmod != oldattr->atttypmod)
-			ereport(ERROR,
-					(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
-					 errmsg("cannot change data type of view column \"%s\" from %s to %s",
-							NameStr(oldattr->attname),
-							format_type_with_typemod(oldattr->atttypid,
-													 oldattr->atttypmod),
-							format_type_with_typemod(newattr->atttypid,
-													 newattr->atttypmod))));
+		{
+			if (is_matview)
+				ereport(ERROR,
+						errcode(ERRCODE_INVALID_TABLE_DEFINITION),
+						errmsg("cannot change data type of materialized view column \"%s\" from %s to %s",
+							   NameStr(oldattr->attname),
+							   format_type_with_typemod(oldattr->atttypid,
+														oldattr->atttypmod),
+							   format_type_with_typemod(newattr->atttypid,
+														newattr->atttypmod)));
+			else
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
+						 errmsg("cannot change data type of view column \"%s\" from %s to %s",
+								NameStr(oldattr->attname),
+								format_type_with_typemod(oldattr->atttypid,
+														 oldattr->atttypmod),
+								format_type_with_typemod(newattr->atttypid,
+														 newattr->atttypmod))));
+		}
 
 		/*
 		 * At this point, attcollations should be both valid or both invalid,
 		 * so applying get_collation_name unconditionally should be fine.
 		 */
 		if (newattr->attcollation != oldattr->attcollation)
-			ereport(ERROR,
-					(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
-					 errmsg("cannot change collation of view column \"%s\" from \"%s\" to \"%s\"",
-							NameStr(oldattr->attname),
-							get_collation_name(oldattr->attcollation),
-							get_collation_name(newattr->attcollation))));
+		{
+			if (is_matview)
+				ereport(ERROR,
+						errcode(ERRCODE_INVALID_TABLE_DEFINITION),
+						errmsg("cannot change collation of materialized view column \"%s\" from \"%s\" to \"%s\"",
+							   NameStr(oldattr->attname),
+							   get_collation_name(oldattr->attcollation),
+							   get_collation_name(newattr->attcollation)));
+			else
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
+						 errmsg("cannot change collation of view column \"%s\" from \"%s\" to \"%s\"",
+								NameStr(oldattr->attname),
+								get_collation_name(oldattr->attcollation),
+								get_collation_name(newattr->attcollation))));
+		}
 	}
 
 	/*
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index dd458182f0..6b32233127 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -4809,6 +4809,21 @@ CreateMatViewStmt:
 					$8->skipData = !($11);
 					$$ = (Node *) ctas;
 				}
+		| CREATE OR REPLACE OptNoLog MATERIALIZED VIEW create_mv_target AS SelectStmt opt_with_data
+				{
+					CreateTableAsStmt *ctas = makeNode(CreateTableAsStmt);
+
+					ctas->query = $9;
+					ctas->into = $7;
+					ctas->objtype = OBJECT_MATVIEW;
+					ctas->is_select_into = false;
+					ctas->if_not_exists = false;
+					/* cram additional flags into the IntoClause */
+					$7->rel->relpersistence = $4;
+					$7->skipData = !($10);
+					$7->replace = true;
+					$$ = (Node *) ctas;
+				}
 		;
 
 create_mv_target:
diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c
index 1be0056af7..122e44267e 100644
--- a/src/bin/psql/tab-complete.in.c
+++ b/src/bin/psql/tab-complete.in.c
@@ -2144,7 +2144,7 @@ match_previous_words(int pattern_id,
 	/* complete with something you can create or replace */
 	else if (TailMatches("CREATE", "OR", "REPLACE"))
 		COMPLETE_WITH("FUNCTION", "PROCEDURE", "LANGUAGE", "RULE", "VIEW",
-					  "AGGREGATE", "TRANSFORM", "TRIGGER");
+					  "AGGREGATE", "TRANSFORM", "TRIGGER", "MATERIALIZED VIEW");
 
 /* DROP, but not DROP embedded in other commands */
 	/* complete with something you can drop */
@@ -3964,13 +3964,16 @@ match_previous_words(int pattern_id,
 		COMPLETE_WITH("SELECT");
 
 /* CREATE MATERIALIZED VIEW */
-	else if (Matches("CREATE", "MATERIALIZED"))
+	else if (Matches("CREATE", "MATERIALIZED") ||
+			 Matches("CREATE", "OR", "REPLACE", "MATERIALIZED"))
 		COMPLETE_WITH("VIEW");
-	/* Complete CREATE MATERIALIZED VIEW <name> with AS */
-	else if (Matches("CREATE", "MATERIALIZED", "VIEW", MatchAny))
+	/* Complete CREATE [ OR REPLACE ] MATERIALIZED VIEW <name> with AS */
+	else if (Matches("CREATE", "MATERIALIZED", "VIEW", MatchAny) ||
+			 Matches("CREATE", "OR", "REPLACE", "MATERIALIZED", "VIEW", MatchAny))
 		COMPLETE_WITH("AS");
-	/* Complete "CREATE MATERIALIZED VIEW <sth> AS with "SELECT" */
-	else if (Matches("CREATE", "MATERIALIZED", "VIEW", MatchAny, "AS"))
+	/* Complete "CREATE [ OR REPLACE ] MATERIALIZED VIEW <sth> AS with "SELECT" */
+	else if (Matches("CREATE", "MATERIALIZED", "VIEW", MatchAny, "AS") ||
+			 Matches("CREATE", "OR", "REPLACE", "MATERIALIZED", "VIEW", MatchAny, "AS"))
 		COMPLETE_WITH("SELECT");
 
 /* CREATE EVENT TRIGGER */
diff --git a/src/include/commands/view.h b/src/include/commands/view.h
index d2d8588989..7eacdaaceb 100644
--- a/src/include/commands/view.h
+++ b/src/include/commands/view.h
@@ -22,4 +22,7 @@ extern ObjectAddress DefineView(ViewStmt *stmt, const char *queryString,
 
 extern void StoreViewQuery(Oid viewOid, Query *viewParse, bool replace);
 
+extern void checkViewColumns(TupleDesc newdesc, TupleDesc olddesc,
+							 bool is_matview);
+
 #endif							/* VIEW_H */
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index b40b661ec8..b71db02c24 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -2358,7 +2358,7 @@ typedef struct AlterTableStmt
 typedef enum AlterTableType
 {
 	AT_AddColumn,				/* add column */
-	AT_AddColumnToView,			/* implicitly via CREATE OR REPLACE VIEW */
+	AT_AddColumnToView,			/* implicitly via CREATE OR REPLACE [MATERIALIZED] VIEW */
 	AT_ColumnDefault,			/* alter column default */
 	AT_CookedColumnDefault,		/* add a pre-cooked column default */
 	AT_DropNotNull,				/* alter column drop not null */
diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h
index b0ef1952e8..533fa847ab 100644
--- a/src/include/nodes/primnodes.h
+++ b/src/include/nodes/primnodes.h
@@ -168,6 +168,7 @@ typedef struct IntoClause
 	/* materialized view's SELECT query */
 	struct Query *viewQuery pg_node_attr(query_jumble_ignore);
 	bool		skipData;		/* true for WITH NO DATA */
+	bool		replace;		/* replace existing matview? */
 } IntoClause;
 
 
diff --git a/src/test/regress/expected/matview.out b/src/test/regress/expected/matview.out
index 038ab73517..e2e2a13396 100644
--- a/src/test/regress/expected/matview.out
+++ b/src/test/regress/expected/matview.out
@@ -694,3 +694,194 @@ NOTICE:  relation "matview_ine_tab" already exists, skipping
 (0 rows)
 
 DROP MATERIALIZED VIEW matview_ine_tab;
+--
+-- test CREATE OR REPLACE MATERIALIZED VIEW
+--
+-- matview does not already exist
+DROP MATERIALIZED VIEW IF EXISTS mvtest_replace;
+NOTICE:  materialized view "mvtest_replace" does not exist, skipping
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 1 AS a;
+SELECT * FROM mvtest_replace;
+ a 
+---
+ 1
+(1 row)
+
+-- replace query with data
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 2 AS a;
+SELECT * FROM mvtest_replace;
+ a 
+---
+ 2
+(1 row)
+
+-- replace query without data
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 3 AS a
+  WITH NO DATA;
+SELECT * FROM mvtest_replace; -- error: not populated
+ERROR:  materialized view "mvtest_replace" has not been populated
+HINT:  Use the REFRESH MATERIALIZED VIEW command.
+REFRESH MATERIALIZED VIEW mvtest_replace;
+SELECT * FROM mvtest_replace;
+ a 
+---
+ 3
+(1 row)
+
+-- add column
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 4 AS a, 1 b;
+SELECT * FROM mvtest_replace;
+ a | b 
+---+---
+ 4 | 1
+(1 row)
+
+-- replace table options
+SELECT m.*, c.relname, c.reloptions, s.spcname, a.amname
+  FROM mvtest_replace m
+    CROSS JOIN pg_class c
+    LEFT JOIN pg_tablespace s ON s.oid = c.reltablespace
+    LEFT JOIN pg_am a ON a.oid = c.relam
+  WHERE c.relname = 'mvtest_replace';
+ a | b |    relname     | reloptions | spcname | amname 
+---+---+----------------+------------+---------+--------
+ 4 | 1 | mvtest_replace |            |         | heap
+(1 row)
+
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace
+  USING heap2
+  WITH (fillfactor = 50)
+  TABLESPACE regress_tblspace
+  AS SELECT 5 AS a, 1 AS b;
+SELECT m.*, c.relname, c.reloptions, s.spcname, a.amname
+  FROM mvtest_replace m
+    CROSS JOIN pg_class c
+    LEFT JOIN pg_tablespace s ON s.oid = c.reltablespace
+    LEFT JOIN pg_am a ON a.oid = c.relam
+  WHERE c.relname = 'mvtest_replace';
+ a | b |    relname     |   reloptions    |     spcname      | amname 
+---+---+----------------+-----------------+------------------+--------
+ 5 | 1 | mvtest_replace | {fillfactor=50} | regress_tblspace | heap2
+(1 row)
+
+-- can replace matview that has a dependent view
+CREATE VIEW mvtest_replace_v AS
+  SELECT * FROM mvtest_replace;
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 6 AS a, 1 AS b;
+SELECT * FROM mvtest_replace, mvtest_replace_v;
+ a | b | a | b 
+---+---+---+---
+ 6 | 1 | 6 | 1
+(1 row)
+
+DROP VIEW mvtest_replace_v;
+-- index gets rebuilt when replacing with data
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 7 AS a, 1 AS b;
+CREATE UNIQUE INDEX ON mvtest_replace (b);
+SELECT * FROM mvtest_replace;
+ a | b 
+---+---
+ 7 | 1
+(1 row)
+
+SET enable_seqscan = off; -- force index scan
+EXPLAIN (COSTS OFF) SELECT * FROM mvtest_replace WHERE b = 1;
+                       QUERY PLAN                        
+---------------------------------------------------------
+ Index Scan using mvtest_replace_b_idx on mvtest_replace
+   Index Cond: (b = 1)
+(2 rows)
+
+SELECT * FROM mvtest_replace WHERE b = 1;
+ a | b 
+---+---
+ 7 | 1
+(1 row)
+
+RESET enable_seqscan;
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 8 AS a, 1 AS b;
+SET enable_seqscan = off; -- force index scan
+EXPLAIN (COSTS OFF) SELECT * FROM mvtest_replace WHERE b = 1;
+                       QUERY PLAN                        
+---------------------------------------------------------
+ Index Scan using mvtest_replace_b_idx on mvtest_replace
+   Index Cond: (b = 1)
+(2 rows)
+
+SELECT * FROM mvtest_replace WHERE b = 1;
+ a | b 
+---+---
+ 8 | 1
+(1 row)
+
+RESET enable_seqscan;
+-- cannot change column data type
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 9 AS a, 'x' AS b; -- error
+ERROR:  cannot change data type of materialized view column "b" from integer to text
+SELECT * FROM mvtest_replace;
+ a | b 
+---+---
+ 8 | 1
+(1 row)
+
+-- cannot rename column
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 10 AS a, 1 AS b2; -- error
+ERROR:  cannot change name of materialized view column "b" to "b2"
+HINT:  Use ALTER MATERIALIZED VIEW ... RENAME COLUMN ... to change name of materialized view column instead.
+SELECT * FROM mvtest_replace;
+ a | b 
+---+---
+ 8 | 1
+(1 row)
+
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 11 AS a, 1 AS b, 'y' COLLATE "C" AS c;
+SELECT * FROM mvtest_replace;
+ a  | b | c 
+----+---+---
+ 11 | 1 | y
+(1 row)
+
+-- cannot change column collation
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 12 AS a, 1 AS b, 'x' COLLATE "POSIX" AS c; -- error
+ERROR:  cannot change collation of materialized view column "c" from "C" to "POSIX"
+SELECT * FROM mvtest_replace;
+ a  | b | c 
+----+---+---
+ 11 | 1 | y
+(1 row)
+
+-- cannot drop column
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 13 AS a, 1 AS b; -- error
+ERROR:  cannot drop columns from materialized view
+SELECT * FROM mvtest_replace;
+ a  | b | c 
+----+---+---
+ 11 | 1 | y
+(1 row)
+
+-- must target a matview
+CREATE VIEW mvtest_not_mv AS
+  SELECT 1 AS a;
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_not_mv AS
+  SELECT 1 AS a; -- error
+ERROR:  "mvtest_not_mv" is not a materialized view
+DROP VIEW mvtest_not_mv;
+-- cannot use OR REPLACE with IF NOT EXISTS
+CREATE OR REPLACE MATERIALIZED VIEW IF NOT EXISTS mvtest_replace AS
+  SELECT 1 AS a;
+ERROR:  syntax error at or near "NOT"
+LINE 1: CREATE OR REPLACE MATERIALIZED VIEW IF NOT EXISTS mvtest_rep...
+                                               ^
+DROP MATERIALIZED VIEW mvtest_replace;
diff --git a/src/test/regress/sql/matview.sql b/src/test/regress/sql/matview.sql
index b74ee305e0..c12f0243c9 100644
--- a/src/test/regress/sql/matview.sql
+++ b/src/test/regress/sql/matview.sql
@@ -314,3 +314,111 @@ EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF)
   CREATE MATERIALIZED VIEW IF NOT EXISTS matview_ine_tab AS
     SELECT 1 / 0 WITH NO DATA; -- ok
 DROP MATERIALIZED VIEW matview_ine_tab;
+
+--
+-- test CREATE OR REPLACE MATERIALIZED VIEW
+--
+
+-- matview does not already exist
+DROP MATERIALIZED VIEW IF EXISTS mvtest_replace;
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 1 AS a;
+SELECT * FROM mvtest_replace;
+
+-- replace query with data
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 2 AS a;
+SELECT * FROM mvtest_replace;
+
+-- replace query without data
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 3 AS a
+  WITH NO DATA;
+SELECT * FROM mvtest_replace; -- error: not populated
+REFRESH MATERIALIZED VIEW mvtest_replace;
+SELECT * FROM mvtest_replace;
+
+-- add column
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 4 AS a, 1 b;
+SELECT * FROM mvtest_replace;
+
+-- replace table options
+SELECT m.*, c.relname, c.reloptions, s.spcname, a.amname
+  FROM mvtest_replace m
+    CROSS JOIN pg_class c
+    LEFT JOIN pg_tablespace s ON s.oid = c.reltablespace
+    LEFT JOIN pg_am a ON a.oid = c.relam
+  WHERE c.relname = 'mvtest_replace';
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace
+  USING heap2
+  WITH (fillfactor = 50)
+  TABLESPACE regress_tblspace
+  AS SELECT 5 AS a, 1 AS b;
+SELECT m.*, c.relname, c.reloptions, s.spcname, a.amname
+  FROM mvtest_replace m
+    CROSS JOIN pg_class c
+    LEFT JOIN pg_tablespace s ON s.oid = c.reltablespace
+    LEFT JOIN pg_am a ON a.oid = c.relam
+  WHERE c.relname = 'mvtest_replace';
+
+-- can replace matview that has a dependent view
+CREATE VIEW mvtest_replace_v AS
+  SELECT * FROM mvtest_replace;
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 6 AS a, 1 AS b;
+SELECT * FROM mvtest_replace, mvtest_replace_v;
+DROP VIEW mvtest_replace_v;
+
+-- index gets rebuilt when replacing with data
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 7 AS a, 1 AS b;
+CREATE UNIQUE INDEX ON mvtest_replace (b);
+SELECT * FROM mvtest_replace;
+SET enable_seqscan = off; -- force index scan
+EXPLAIN (COSTS OFF) SELECT * FROM mvtest_replace WHERE b = 1;
+SELECT * FROM mvtest_replace WHERE b = 1;
+RESET enable_seqscan;
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 8 AS a, 1 AS b;
+SET enable_seqscan = off; -- force index scan
+EXPLAIN (COSTS OFF) SELECT * FROM mvtest_replace WHERE b = 1;
+SELECT * FROM mvtest_replace WHERE b = 1;
+RESET enable_seqscan;
+
+-- cannot change column data type
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 9 AS a, 'x' AS b; -- error
+SELECT * FROM mvtest_replace;
+
+-- cannot rename column
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 10 AS a, 1 AS b2; -- error
+SELECT * FROM mvtest_replace;
+
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 11 AS a, 1 AS b, 'y' COLLATE "C" AS c;
+SELECT * FROM mvtest_replace;
+
+-- cannot change column collation
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 12 AS a, 1 AS b, 'x' COLLATE "POSIX" AS c; -- error
+SELECT * FROM mvtest_replace;
+
+-- cannot drop column
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 13 AS a, 1 AS b; -- error
+SELECT * FROM mvtest_replace;
+
+-- must target a matview
+CREATE VIEW mvtest_not_mv AS
+  SELECT 1 AS a;
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_not_mv AS
+  SELECT 1 AS a; -- error
+DROP VIEW mvtest_not_mv;
+
+-- cannot use OR REPLACE with IF NOT EXISTS
+CREATE OR REPLACE MATERIALIZED VIEW IF NOT EXISTS mvtest_replace AS
+  SELECT 1 AS a;
+
+DROP MATERIALIZED VIEW mvtest_replace;
-- 
2.47.0

  [text/x-diff] v4-0002-Deprecate-CREATE-MATERIALIZED-VIEW-IF-NOT-EXISTS.patch (11.0K, ../../d4bf3ccf-5c2b-448d-ad16-59dd6fbe2a2e@ewie.name/3-v4-0002-Deprecate-CREATE-MATERIALIZED-VIEW-IF-NOT-EXISTS.patch)
  download | inline diff:
From ce6d3bbdac073b4750745120f9a754c30bd57b66 Mon Sep 17 00:00:00 2001
From: Erik Wienhold <ewie@ewie.name>
Date: Tue, 28 May 2024 02:19:53 +0200
Subject: [PATCH v4 2/3] Deprecate CREATE MATERIALIZED VIEW IF NOT EXISTS

---
 src/backend/parser/gram.y                     | 14 ++++++
 .../expected/test_extensions.out              | 45 +++++++++++++++++++
 src/test/regress/expected/matview.out         | 20 +++++++++
 3 files changed, 79 insertions(+)

diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 6b32233127..dadc6b630a 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -4808,6 +4808,20 @@ CreateMatViewStmt:
 					$8->rel->relpersistence = $2;
 					$8->skipData = !($11);
 					$$ = (Node *) ctas;
+
+					if (ctas->into->rel->schemaname)
+							ereport(WARNING,
+									errmsg("IF NOT EXISTS is deprecated in materialized view creation"),
+									errhint("Use CREATE OR REPLACE MATERIALIZED VIEW %s.%s.",
+											ctas->into->rel->schemaname,
+											ctas->into->rel->relname),
+									parser_errposition(@1));
+					else
+							ereport(WARNING,
+									errmsg("IF NOT EXISTS is deprecated in materialized view creation"),
+									errhint("Use CREATE OR REPLACE MATERIALIZED VIEW %s.",
+											ctas->into->rel->relname),
+									parser_errposition(@1));
 				}
 		| CREATE OR REPLACE OptNoLog MATERIALIZED VIEW create_mv_target AS SelectStmt opt_with_data
 				{
diff --git a/src/test/modules/test_extensions/expected/test_extensions.out b/src/test/modules/test_extensions/expected/test_extensions.out
index d5388a1fec..9a1e0f7658 100644
--- a/src/test/modules/test_extensions/expected/test_extensions.out
+++ b/src/test/modules/test_extensions/expected/test_extensions.out
@@ -404,6 +404,11 @@ Objects in extension "test_ext_cor"
 CREATE COLLATION ext_cine_coll
   ( LC_COLLATE = "C", LC_CTYPE = "C" );
 CREATE EXTENSION test_ext_cine;  -- fail
+WARNING:  IF NOT EXISTS is deprecated in materialized view creation
+LINE 1: CREATE MATERIALIZED VIEW IF NOT EXISTS ext_cine_mv AS SELECT...
+        ^
+HINT:  Use CREATE OR REPLACE MATERIALIZED VIEW ext_cine_mv.
+QUERY:  CREATE MATERIALIZED VIEW IF NOT EXISTS ext_cine_mv AS SELECT 42 AS f1;
 ERROR:  collation ext_cine_coll is not a member of extension "test_ext_cine"
 DETAIL:  An extension may only use CREATE ... IF NOT EXISTS to skip object creation if the conflicting object is one that it already owns.
 CONTEXT:  SQL statement "CREATE COLLATION IF NOT EXISTS ext_cine_coll
@@ -412,6 +417,11 @@ extension script file "test_ext_cine--1.0.sql", near line 10
 DROP COLLATION ext_cine_coll;
 CREATE MATERIALIZED VIEW ext_cine_mv AS SELECT 11 AS f1;
 CREATE EXTENSION test_ext_cine;  -- fail
+WARNING:  IF NOT EXISTS is deprecated in materialized view creation
+LINE 1: CREATE MATERIALIZED VIEW IF NOT EXISTS ext_cine_mv AS SELECT...
+        ^
+HINT:  Use CREATE OR REPLACE MATERIALIZED VIEW ext_cine_mv.
+QUERY:  CREATE MATERIALIZED VIEW IF NOT EXISTS ext_cine_mv AS SELECT 42 AS f1;
 ERROR:  materialized view ext_cine_mv is not a member of extension "test_ext_cine"
 DETAIL:  An extension may only use CREATE ... IF NOT EXISTS to skip object creation if the conflicting object is one that it already owns.
 CONTEXT:  SQL statement "CREATE MATERIALIZED VIEW IF NOT EXISTS ext_cine_mv AS SELECT 42 AS f1"
@@ -420,6 +430,11 @@ DROP MATERIALIZED VIEW ext_cine_mv;
 CREATE FOREIGN DATA WRAPPER dummy;
 CREATE SERVER ext_cine_srv FOREIGN DATA WRAPPER dummy;
 CREATE EXTENSION test_ext_cine;  -- fail
+WARNING:  IF NOT EXISTS is deprecated in materialized view creation
+LINE 1: CREATE MATERIALIZED VIEW IF NOT EXISTS ext_cine_mv AS SELECT...
+        ^
+HINT:  Use CREATE OR REPLACE MATERIALIZED VIEW ext_cine_mv.
+QUERY:  CREATE MATERIALIZED VIEW IF NOT EXISTS ext_cine_mv AS SELECT 42 AS f1;
 ERROR:  server ext_cine_srv is not a member of extension "test_ext_cine"
 DETAIL:  An extension may only use CREATE ... IF NOT EXISTS to skip object creation if the conflicting object is one that it already owns.
 CONTEXT:  SQL statement "CREATE SERVER IF NOT EXISTS ext_cine_srv FOREIGN DATA WRAPPER ext_cine_fdw"
@@ -427,6 +442,11 @@ extension script file "test_ext_cine--1.0.sql", near line 17
 DROP SERVER ext_cine_srv;
 CREATE SCHEMA ext_cine_schema;
 CREATE EXTENSION test_ext_cine;  -- fail
+WARNING:  IF NOT EXISTS is deprecated in materialized view creation
+LINE 1: CREATE MATERIALIZED VIEW IF NOT EXISTS ext_cine_mv AS SELECT...
+        ^
+HINT:  Use CREATE OR REPLACE MATERIALIZED VIEW ext_cine_mv.
+QUERY:  CREATE MATERIALIZED VIEW IF NOT EXISTS ext_cine_mv AS SELECT 42 AS f1;
 ERROR:  schema ext_cine_schema is not a member of extension "test_ext_cine"
 DETAIL:  An extension may only use CREATE ... IF NOT EXISTS to skip object creation if the conflicting object is one that it already owns.
 CONTEXT:  SQL statement "CREATE SCHEMA IF NOT EXISTS ext_cine_schema"
@@ -434,6 +454,11 @@ extension script file "test_ext_cine--1.0.sql", near line 19
 DROP SCHEMA ext_cine_schema;
 CREATE SEQUENCE ext_cine_seq;
 CREATE EXTENSION test_ext_cine;  -- fail
+WARNING:  IF NOT EXISTS is deprecated in materialized view creation
+LINE 1: CREATE MATERIALIZED VIEW IF NOT EXISTS ext_cine_mv AS SELECT...
+        ^
+HINT:  Use CREATE OR REPLACE MATERIALIZED VIEW ext_cine_mv.
+QUERY:  CREATE MATERIALIZED VIEW IF NOT EXISTS ext_cine_mv AS SELECT 42 AS f1;
 ERROR:  sequence ext_cine_seq is not a member of extension "test_ext_cine"
 DETAIL:  An extension may only use CREATE ... IF NOT EXISTS to skip object creation if the conflicting object is one that it already owns.
 CONTEXT:  SQL statement "CREATE SEQUENCE IF NOT EXISTS ext_cine_seq"
@@ -441,6 +466,11 @@ extension script file "test_ext_cine--1.0.sql", near line 21
 DROP SEQUENCE ext_cine_seq;
 CREATE TABLE ext_cine_tab1 (x int);
 CREATE EXTENSION test_ext_cine;  -- fail
+WARNING:  IF NOT EXISTS is deprecated in materialized view creation
+LINE 1: CREATE MATERIALIZED VIEW IF NOT EXISTS ext_cine_mv AS SELECT...
+        ^
+HINT:  Use CREATE OR REPLACE MATERIALIZED VIEW ext_cine_mv.
+QUERY:  CREATE MATERIALIZED VIEW IF NOT EXISTS ext_cine_mv AS SELECT 42 AS f1;
 ERROR:  table ext_cine_tab1 is not a member of extension "test_ext_cine"
 DETAIL:  An extension may only use CREATE ... IF NOT EXISTS to skip object creation if the conflicting object is one that it already owns.
 CONTEXT:  SQL statement "CREATE TABLE IF NOT EXISTS ext_cine_tab1 (x int)"
@@ -448,12 +478,22 @@ extension script file "test_ext_cine--1.0.sql", near line 23
 DROP TABLE ext_cine_tab1;
 CREATE TABLE ext_cine_tab2 AS SELECT 42 AS y;
 CREATE EXTENSION test_ext_cine;  -- fail
+WARNING:  IF NOT EXISTS is deprecated in materialized view creation
+LINE 1: CREATE MATERIALIZED VIEW IF NOT EXISTS ext_cine_mv AS SELECT...
+        ^
+HINT:  Use CREATE OR REPLACE MATERIALIZED VIEW ext_cine_mv.
+QUERY:  CREATE MATERIALIZED VIEW IF NOT EXISTS ext_cine_mv AS SELECT 42 AS f1;
 ERROR:  table ext_cine_tab2 is not a member of extension "test_ext_cine"
 DETAIL:  An extension may only use CREATE ... IF NOT EXISTS to skip object creation if the conflicting object is one that it already owns.
 CONTEXT:  SQL statement "CREATE TABLE IF NOT EXISTS ext_cine_tab2 AS SELECT 42 AS y"
 extension script file "test_ext_cine--1.0.sql", near line 25
 DROP TABLE ext_cine_tab2;
 CREATE EXTENSION test_ext_cine;
+WARNING:  IF NOT EXISTS is deprecated in materialized view creation
+LINE 1: CREATE MATERIALIZED VIEW IF NOT EXISTS ext_cine_mv AS SELECT...
+        ^
+HINT:  Use CREATE OR REPLACE MATERIALIZED VIEW ext_cine_mv.
+QUERY:  CREATE MATERIALIZED VIEW IF NOT EXISTS ext_cine_mv AS SELECT 42 AS f1;
 \dx+ test_ext_cine
 Objects in extension "test_ext_cine"
         Object description         
@@ -475,6 +515,11 @@ Objects in extension "test_ext_cine"
 (14 rows)
 
 ALTER EXTENSION test_ext_cine UPDATE TO '1.1';
+WARNING:  IF NOT EXISTS is deprecated in materialized view creation
+LINE 1: CREATE MATERIALIZED VIEW IF NOT EXISTS ext_cine_mv AS SELECT...
+        ^
+HINT:  Use CREATE OR REPLACE MATERIALIZED VIEW ext_cine_mv.
+QUERY:  CREATE MATERIALIZED VIEW IF NOT EXISTS ext_cine_mv AS SELECT 42 AS f1;
 \dx+ test_ext_cine
 Objects in extension "test_ext_cine"
         Object description         
diff --git a/src/test/regress/expected/matview.out b/src/test/regress/expected/matview.out
index e2e2a13396..cefd0d442c 100644
--- a/src/test/regress/expected/matview.out
+++ b/src/test/regress/expected/matview.out
@@ -565,6 +565,10 @@ CREATE MATERIALIZED VIEW mvtest_mv_foo AS SELECT * FROM mvtest_foo_data;
 CREATE MATERIALIZED VIEW mvtest_mv_foo AS SELECT * FROM mvtest_foo_data;
 ERROR:  relation "mvtest_mv_foo" already exists
 CREATE MATERIALIZED VIEW IF NOT EXISTS mvtest_mv_foo AS SELECT * FROM mvtest_foo_data;
+WARNING:  IF NOT EXISTS is deprecated in materialized view creation
+LINE 1: CREATE MATERIALIZED VIEW IF NOT EXISTS mvtest_mv_foo AS SELE...
+        ^
+HINT:  Use CREATE OR REPLACE MATERIALIZED VIEW mvtest_mv_foo.
 NOTICE:  relation "mvtest_mv_foo" already exists, skipping
 CREATE UNIQUE INDEX ON mvtest_mv_foo (i);
 RESET ROLE;
@@ -662,12 +666,20 @@ CREATE MATERIALIZED VIEW matview_ine_tab AS SELECT 1 / 0; -- error
 ERROR:  relation "matview_ine_tab" already exists
 CREATE MATERIALIZED VIEW IF NOT EXISTS matview_ine_tab AS
   SELECT 1 / 0; -- ok
+WARNING:  IF NOT EXISTS is deprecated in materialized view creation
+LINE 1: CREATE MATERIALIZED VIEW IF NOT EXISTS matview_ine_tab AS
+        ^
+HINT:  Use CREATE OR REPLACE MATERIALIZED VIEW matview_ine_tab.
 NOTICE:  relation "matview_ine_tab" already exists, skipping
 CREATE MATERIALIZED VIEW matview_ine_tab AS
   SELECT 1 / 0 WITH NO DATA; -- error
 ERROR:  relation "matview_ine_tab" already exists
 CREATE MATERIALIZED VIEW IF NOT EXISTS matview_ine_tab AS
   SELECT 1 / 0 WITH NO DATA; -- ok
+WARNING:  IF NOT EXISTS is deprecated in materialized view creation
+LINE 1: CREATE MATERIALIZED VIEW IF NOT EXISTS matview_ine_tab AS
+        ^
+HINT:  Use CREATE OR REPLACE MATERIALIZED VIEW matview_ine_tab.
 NOTICE:  relation "matview_ine_tab" already exists, skipping
 EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF)
   CREATE MATERIALIZED VIEW matview_ine_tab AS
@@ -676,6 +688,10 @@ ERROR:  relation "matview_ine_tab" already exists
 EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF)
   CREATE MATERIALIZED VIEW IF NOT EXISTS matview_ine_tab AS
     SELECT 1 / 0; -- ok
+WARNING:  IF NOT EXISTS is deprecated in materialized view creation
+LINE 2:   CREATE MATERIALIZED VIEW IF NOT EXISTS matview_ine_tab AS
+          ^
+HINT:  Use CREATE OR REPLACE MATERIALIZED VIEW matview_ine_tab.
 NOTICE:  relation "matview_ine_tab" already exists, skipping
  QUERY PLAN 
 ------------
@@ -688,6 +704,10 @@ ERROR:  relation "matview_ine_tab" already exists
 EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF)
   CREATE MATERIALIZED VIEW IF NOT EXISTS matview_ine_tab AS
     SELECT 1 / 0 WITH NO DATA; -- ok
+WARNING:  IF NOT EXISTS is deprecated in materialized view creation
+LINE 2:   CREATE MATERIALIZED VIEW IF NOT EXISTS matview_ine_tab AS
+          ^
+HINT:  Use CREATE OR REPLACE MATERIALIZED VIEW matview_ine_tab.
 NOTICE:  relation "matview_ine_tab" already exists, skipping
  QUERY PLAN 
 ------------
-- 
2.47.0

  [text/x-diff] v4-0003-Replace-matview-WITH-OLD-DATA.patch (7.7K, ../../d4bf3ccf-5c2b-448d-ad16-59dd6fbe2a2e@ewie.name/4-v4-0003-Replace-matview-WITH-OLD-DATA.patch)
  download | inline diff:
From 33fe1e0d676fb4fdde43ed687dc66e98f7f8b6d5 Mon Sep 17 00:00:00 2001
From: Erik Wienhold <ewie@ewie.name>
Date: Fri, 26 Jul 2024 23:33:15 +0200
Subject: [PATCH v4 3/3] Replace matview WITH OLD DATA

---
 .../sgml/ref/create_materialized_view.sgml    | 16 +++++++++--
 src/backend/commands/createas.c               | 26 +++++++++++------
 src/backend/parser/gram.y                     | 16 +++++++++++
 src/include/nodes/primnodes.h                 |  1 +
 src/test/regress/expected/matview.out         | 28 +++++++++++++++++++
 src/test/regress/sql/matview.sql              | 15 ++++++++++
 6 files changed, 90 insertions(+), 12 deletions(-)

diff --git a/doc/src/sgml/ref/create_materialized_view.sgml b/doc/src/sgml/ref/create_materialized_view.sgml
index 5e03320eb7..1352e9de40 100644
--- a/doc/src/sgml/ref/create_materialized_view.sgml
+++ b/doc/src/sgml/ref/create_materialized_view.sgml
@@ -27,7 +27,7 @@ CREATE [ OR REPLACE ] MATERIALIZED VIEW [ IF NOT EXISTS ] <replaceable>table_nam
     [ WITH ( <replaceable class="parameter">storage_parameter</replaceable> [= <replaceable class="parameter">value</replaceable>] [, ... ] ) ]
     [ TABLESPACE <replaceable class="parameter">tablespace_name</replaceable> ]
     AS <replaceable>query</replaceable>
-    [ WITH [ NO ] DATA ]
+    [ WITH [ NO | OLD ] DATA ]
 </synopsis>
  </refsynopsisdiv>
 
@@ -37,7 +37,8 @@ CREATE [ OR REPLACE ] MATERIALIZED VIEW [ IF NOT EXISTS ] <replaceable>table_nam
   <para>
    <command>CREATE MATERIALIZED VIEW</command> defines a materialized view of
    a query.  The query is executed and used to populate the view at the time
-   the command is issued (unless <command>WITH NO DATA</command> is used) and may be
+   the command is issued (unless <command>WITH NO DATA</command> or
+   <command>WITH OLD DATA</command> is used) and may be
    refreshed later using <command>REFRESH MATERIALIZED VIEW</command>.
   </para>
 
@@ -162,7 +163,7 @@ CREATE [ OR REPLACE ] MATERIALIZED VIEW [ IF NOT EXISTS ] <replaceable>table_nam
    </varlistentry>
 
    <varlistentry>
-    <term><literal>WITH [ NO ] DATA</literal></term>
+    <term><literal>WITH [ NO | OLD ] DATA</literal></term>
     <listitem>
      <para>
       This clause specifies whether or not the materialized view should be
@@ -170,6 +171,15 @@ CREATE [ OR REPLACE ] MATERIALIZED VIEW [ IF NOT EXISTS ] <replaceable>table_nam
       flagged as unscannable and cannot be queried until <command>REFRESH
       MATERIALIZED VIEW</command> is used.
      </para>
+
+     <para>
+      The form <command>WITH OLD DATA</command> keeps the already stored data
+      when replacing an existing materialized view to keep it populated.  For
+      newly created materialized views, this has the same effect as
+      <command>WITH DATA</command>.  Use this form if you want to use
+      <command>REFRESH MATERIALIZED VIEW CONCURRENTLY</command> as it requires
+      a populated materialized view.
+     </para>
     </listitem>
    </varlistentry>
 
diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c
index d4bc5e5c08..8db000554c 100644
--- a/src/backend/commands/createas.c
+++ b/src/backend/commands/createas.c
@@ -332,18 +332,26 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt,
 		/* An existing materialized view can be replaced. */
 		if (is_matview && into->replace)
 		{
-			RefreshMatViewStmt *refresh;
-
 			/* Change the relation to match the new query and other options. */
-			(void) create_ctas_nodata(query->targetList, into);
+			address = create_ctas_nodata(query->targetList, into);
 
-			/* Refresh the materialized view with a fake statement. */
-			refresh = makeNode(RefreshMatViewStmt);
-			refresh->relation = into->rel;
-			refresh->skipData = into->skipData;
-			refresh->concurrent = false;
+			/*
+			 * Refresh the materialized view with a fake statement unless we
+			 * must keep the old data.
+			 */
+			if (!into->keepData)
+			{
+				RefreshMatViewStmt *refresh;
+
+				refresh = makeNode(RefreshMatViewStmt);
+				refresh->relation = into->rel;
+				refresh->skipData = into->skipData;
+				refresh->concurrent = false;
+
+				address = ExecRefreshMatView(refresh, NULL, NULL);
+			}
 
-			return ExecRefreshMatView(refresh, NULL, NULL);
+			return address;
 		}
 
 		return InvalidObjectAddress;
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index dadc6b630a..1be1a95cd9 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -4838,6 +4838,22 @@ CreateMatViewStmt:
 					$7->replace = true;
 					$$ = (Node *) ctas;
 				}
+		| CREATE OR REPLACE OptNoLog MATERIALIZED VIEW create_mv_target AS SelectStmt WITH OLD DATA_P
+				{
+					CreateTableAsStmt *ctas = makeNode(CreateTableAsStmt);
+
+					ctas->query = $9;
+					ctas->into = $7;
+					ctas->objtype = OBJECT_MATVIEW;
+					ctas->is_select_into = false;
+					ctas->if_not_exists = false;
+					/* cram additional flags into the IntoClause */
+					$7->rel->relpersistence = $4;
+					$7->skipData = false;
+					$7->keepData = true;
+					$7->replace = true;
+					$$ = (Node *) ctas;
+				}
 		;
 
 create_mv_target:
diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h
index 533fa847ab..6fa3291c64 100644
--- a/src/include/nodes/primnodes.h
+++ b/src/include/nodes/primnodes.h
@@ -168,6 +168,7 @@ typedef struct IntoClause
 	/* materialized view's SELECT query */
 	struct Query *viewQuery pg_node_attr(query_jumble_ignore);
 	bool		skipData;		/* true for WITH NO DATA */
+	bool		keepData;		/* true for WITH OLD DATA */
 	bool		replace;		/* replace existing matview? */
 } IntoClause;
 
diff --git a/src/test/regress/expected/matview.out b/src/test/regress/expected/matview.out
index cefd0d442c..47dfd88bff 100644
--- a/src/test/regress/expected/matview.out
+++ b/src/test/regress/expected/matview.out
@@ -751,6 +751,23 @@ SELECT * FROM mvtest_replace;
  3
 (1 row)
 
+-- replace query but keep old data
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 5 AS a
+  WITH OLD DATA;
+SELECT * FROM mvtest_replace;
+ a 
+---
+ 3
+(1 row)
+
+REFRESH MATERIALIZED VIEW mvtest_replace;
+SELECT * FROM mvtest_replace;
+ a 
+---
+ 5
+(1 row)
+
 -- add column
 CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
   SELECT 4 AS a, 1 b;
@@ -905,3 +922,14 @@ ERROR:  syntax error at or near "NOT"
 LINE 1: CREATE OR REPLACE MATERIALIZED VIEW IF NOT EXISTS mvtest_rep...
                                                ^
 DROP MATERIALIZED VIEW mvtest_replace;
+-- Create new matview WITH OLD DATA.  This populates the new matview as if
+-- WITH DATA had been specified.
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 17 AS a
+  WITH OLD DATA;
+SELECT * FROM mvtest_replace;
+ a  
+----
+ 17
+(1 row)
+
diff --git a/src/test/regress/sql/matview.sql b/src/test/regress/sql/matview.sql
index c12f0243c9..b268237c24 100644
--- a/src/test/regress/sql/matview.sql
+++ b/src/test/regress/sql/matview.sql
@@ -338,6 +338,14 @@ SELECT * FROM mvtest_replace; -- error: not populated
 REFRESH MATERIALIZED VIEW mvtest_replace;
 SELECT * FROM mvtest_replace;
 
+-- replace query but keep old data
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 5 AS a
+  WITH OLD DATA;
+SELECT * FROM mvtest_replace;
+REFRESH MATERIALIZED VIEW mvtest_replace;
+SELECT * FROM mvtest_replace;
+
 -- add column
 CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
   SELECT 4 AS a, 1 b;
@@ -422,3 +430,10 @@ CREATE OR REPLACE MATERIALIZED VIEW IF NOT EXISTS mvtest_replace AS
   SELECT 1 AS a;
 
 DROP MATERIALIZED VIEW mvtest_replace;
+
+-- Create new matview WITH OLD DATA.  This populates the new matview as if
+-- WITH DATA had been specified.
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 17 AS a
+  WITH OLD DATA;
+SELECT * FROM mvtest_replace;
-- 
2.47.0

^ permalink  raw  reply  [nested|flat] 29+ messages in thread

* Re: CREATE OR REPLACE MATERIALIZED VIEW
@ 2024-10-31 03:48  Michael Paquier <michael@paquier.xyz>
  parent: Aleksander Alekseev <aleksander@timescale.com>
  0 siblings, 0 replies; 29+ messages in thread

From: Michael Paquier @ 2024-10-31 03:48 UTC (permalink / raw)
  To: Aleksander Alekseev <aleksander@timescale.com>; +Cc: pgsql-hackers@postgresql.org, Erik Wienhold <ewie@ewie.name>

On Tue, Jul 02, 2024 at 01:46:21PM +0300, Aleksander Alekseev wrote:
> I can imagine how this may impact many applications and upset many
> software developers worldwide. Was there even a precedent (in the
> recent decade or so) when PostgreSQL broke the SQL syntax?

We're usually very careful about that, and maintaining a
backward-compatible grammar has a minimal cost in the parser.  The
closest thing I can think of that has a rather complicated grammar is
COPY, which has *two* legacy grammars still supported, one for ~7.3
and one for ~9.0.

> To clarify, I'm not opposed to this idea. If we are fine with breaking
> backward compatibility on the SQL level, this would allow dropping the
> support of inherited tables some day, a feature that in my humble
> opinion shouldn't exist (I realize this is another and very debatable
> question though). I just don't think this is something we ever do in
> this project. But I admit that this information may be incorrect
> and/or outdated.

I am not sure that there is much to gain with this proposal knowing
that the commands with matviews have been around for quite a long time
now.  Particularly, after looking at 0001, you'd see that it shortcuts
a couple of areas of the CTAS code because that's what we are relying
on when building the initial data of matviews.  Hence,
implementation-wise in the backend, matviews are much closer to
physical relations than views.  This is trying to make matviews behave
more consistently with views.

This topic has been mentioned once on pgsql-general back in 2019, for
reference:
https://www.postgresql.org/message-id/CAAWhf%3DPHch2H3ekYnbafuwqWqwyRok8WVPaDxKosZE4GQ2pq5w%40mail.g...
--
Michael

Attachments:

  [application/pgp-signature] signature.asc (832B, ../../ZyL-BexBTGJ6Nkat@paquier.xyz/2-signature.asc)
  download

^ permalink  raw  reply  [nested|flat] 29+ messages in thread

* Re: CREATE OR REPLACE MATERIALIZED VIEW
@ 2025-01-12 21:33  Erik Wienhold <ewie@ewie.name>
  parent: Erik Wienhold <ewie@ewie.name>
  0 siblings, 1 reply; 29+ messages in thread

From: Erik Wienhold @ 2025-01-12 21:33 UTC (permalink / raw)
  To: Said Assemlal <sassemlal@neurorx.com>; +Cc: pgsql-hackers

Here's a rebased v5 due to conflicts with de1e298857.  No other changes
since v4.

-- 
Erik Wienhold
From ac9bba0960f7a6fa507020400f1b4bcf4c9a25d3 Mon Sep 17 00:00:00 2001
From: Erik Wienhold <ewie@ewie.name>
Date: Tue, 21 May 2024 18:35:47 +0200
Subject: [PATCH v5 1/3] Add CREATE OR REPLACE MATERIALIZED VIEW

---
 .../sgml/ref/create_materialized_view.sgml    |  15 +-
 src/backend/commands/createas.c               | 207 ++++++++++++++----
 src/backend/commands/tablecmds.c              |   8 +-
 src/backend/commands/view.c                   | 106 ++++++---
 src/backend/parser/gram.y                     |  15 ++
 src/bin/psql/tab-complete.in.c                |  26 ++-
 src/include/commands/view.h                   |   3 +
 src/include/nodes/parsenodes.h                |   2 +-
 src/include/nodes/primnodes.h                 |   1 +
 src/test/regress/expected/matview.out         | 191 ++++++++++++++++
 src/test/regress/sql/matview.sql              | 108 +++++++++
 11 files changed, 589 insertions(+), 93 deletions(-)

diff --git a/doc/src/sgml/ref/create_materialized_view.sgml b/doc/src/sgml/ref/create_materialized_view.sgml
index 62d897931c..5e03320eb7 100644
--- a/doc/src/sgml/ref/create_materialized_view.sgml
+++ b/doc/src/sgml/ref/create_materialized_view.sgml
@@ -21,7 +21,7 @@ PostgreSQL documentation
 
  <refsynopsisdiv>
 <synopsis>
-CREATE MATERIALIZED VIEW [ IF NOT EXISTS ] <replaceable>table_name</replaceable>
+CREATE [ OR REPLACE ] MATERIALIZED VIEW [ IF NOT EXISTS ] <replaceable>table_name</replaceable>
     [ (<replaceable>column_name</replaceable> [, ...] ) ]
     [ USING <replaceable class="parameter">method</replaceable> ]
     [ WITH ( <replaceable class="parameter">storage_parameter</replaceable> [= <replaceable class="parameter">value</replaceable>] [, ... ] ) ]
@@ -60,6 +60,17 @@ CREATE MATERIALIZED VIEW [ IF NOT EXISTS ] <replaceable>table_name</replaceable>
   <title>Parameters</title>
 
   <variablelist>
+   <varlistentry>
+    <term><literal>OR REPLACE</literal></term>
+    <listitem>
+     <para>
+      Replaces a materialized view if it already exists.
+      Specifying <literal>OR REPLACE</literal> together with
+      <literal>IF NOT EXISTS</literal> is an error.
+     </para>
+    </listitem>
+   </varlistentry>
+
    <varlistentry>
     <term><literal>IF NOT EXISTS</literal></term>
     <listitem>
@@ -67,7 +78,7 @@ CREATE MATERIALIZED VIEW [ IF NOT EXISTS ] <replaceable>table_name</replaceable>
       Do not throw an error if a materialized view with the same name already
       exists. A notice is issued in this case.  Note that there is no guarantee
       that the existing materialized view is anything like the one that would
-      have been created.
+      have been created, unless you use <literal>OR REPLACE</literal> instead.
      </para>
     </listitem>
    </varlistentry>
diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c
index 23cecd99c9..cba369114b 100644
--- a/src/backend/commands/createas.c
+++ b/src/backend/commands/createas.c
@@ -79,55 +79,151 @@ static void intorel_destroy(DestReceiver *self);
 static ObjectAddress
 create_ctas_internal(List *attrList, IntoClause *into)
 {
-	CreateStmt *create = makeNode(CreateStmt);
-	bool		is_matview;
+	bool		is_matview,
+				replace = false;
 	char		relkind;
-	Datum		toast_options;
-	const char *const validnsps[] = HEAP_RELOPT_NAMESPACES;
+	Oid			matviewOid = InvalidOid;
 	ObjectAddress intoRelationAddr;
 
 	/* This code supports both CREATE TABLE AS and CREATE MATERIALIZED VIEW */
 	is_matview = (into->viewQuery != NULL);
 	relkind = is_matview ? RELKIND_MATVIEW : RELKIND_RELATION;
 
-	/*
-	 * Create the target relation by faking up a CREATE TABLE parsetree and
-	 * passing it to DefineRelation.
-	 */
-	create->relation = into->rel;
-	create->tableElts = attrList;
-	create->inhRelations = NIL;
-	create->ofTypename = NULL;
-	create->constraints = NIL;
-	create->options = into->options;
-	create->oncommit = into->onCommit;
-	create->tablespacename = into->tableSpaceName;
-	create->if_not_exists = false;
-	create->accessMethod = into->accessMethod;
+	/* Check if an existing materialized view needs to be replaced. */
+	if (is_matview)
+	{
+		LOCKMODE	lockmode;
 
-	/*
-	 * Create the relation.  (This will error out if there's an existing view,
-	 * so we don't need more code to complain if "replace" is false.)
-	 */
-	intoRelationAddr = DefineRelation(create, relkind, InvalidOid, NULL, NULL);
+		lockmode = into->replace ? AccessExclusiveLock : NoLock;
+		(void) RangeVarGetAndCheckCreationNamespace(into->rel, lockmode,
+													&matviewOid);
+		replace = OidIsValid(matviewOid) && into->replace;
+	}
 
-	/*
-	 * If necessary, create a TOAST table for the target table.  Note that
-	 * NewRelationCreateToastTable ends with CommandCounterIncrement(), so
-	 * that the TOAST table will be visible for insertion.
-	 */
-	CommandCounterIncrement();
+	if (is_matview && replace)
+	{
+		Relation	rel;
+		List	   *atcmds = NIL;
+		AlterTableCmd *atcmd;
+		TupleDesc	descriptor;
+
+		rel = relation_open(matviewOid, NoLock);
+
+		if (rel->rd_rel->relkind != RELKIND_MATVIEW)
+			ereport(ERROR,
+					errcode(ERRCODE_WRONG_OBJECT_TYPE),
+					errmsg("\"%s\" is not a materialized view",
+						   RelationGetRelationName(rel)));
+
+		CheckTableNotInUse(rel, "CREATE OR REPLACE MATERIALIZED VIEW");
+
+		descriptor = BuildDescForRelation(attrList);
+		checkViewColumns(descriptor, rel->rd_att, true);
+
+		/* Add new attributes via ALTER TABLE. */
+		if (list_length(attrList) > rel->rd_att->natts)
+		{
+			ListCell   *c;
+			int			skip = rel->rd_att->natts;
+
+			foreach(c, attrList)
+			{
+				if (skip > 0)
+				{
+					skip--;
+					continue;
+				}
+				atcmd = makeNode(AlterTableCmd);
+				atcmd->subtype = AT_AddColumnToView;
+				atcmd->def = (Node *) lfirst(c);
+				atcmds = lappend(atcmds, atcmd);
+			}
+		}
+
+		/* Set access method via ALTER TABLE. */
+		if (into->accessMethod != NULL)
+		{
+			atcmd = makeNode(AlterTableCmd);
+			atcmd->subtype = AT_SetAccessMethod;
+			atcmd->name = into->accessMethod;
+			atcmds = lappend(atcmds, atcmd);
+		}
+
+		/* Set tablespace via ALTER TABLE. */
+		if (into->tableSpaceName != NULL)
+		{
+			atcmd = makeNode(AlterTableCmd);
+			atcmd->subtype = AT_SetTableSpace;
+			atcmd->name = into->tableSpaceName;
+			atcmds = lappend(atcmds, atcmd);
+		}
+
+		/* Set storage parameters via ALTER TABLE. */
+		if (into->options != NIL)
+		{
+			atcmd = makeNode(AlterTableCmd);
+			atcmd->subtype = AT_ReplaceRelOptions;
+			atcmd->def = (Node *) into->options;
+			atcmds = lappend(atcmds, atcmd);
+		}
+
+		if (atcmds != NIL)
+		{
+			AlterTableInternal(matviewOid, atcmds, true);
+			CommandCounterIncrement();
+		}
+
+		relation_close(rel, NoLock);
+
+		ObjectAddressSet(intoRelationAddr, RelationRelationId, matviewOid);
+	}
+	else
+	{
+		CreateStmt *create = makeNode(CreateStmt);
+		Datum		toast_options;
+		const static char *validnsps[] = HEAP_RELOPT_NAMESPACES;
+
+		/*
+		 * Create the target relation by faking up a CREATE TABLE parsetree
+		 * and passing it to DefineRelation.
+		 */
+		create->relation = into->rel;
+		create->tableElts = attrList;
+		create->inhRelations = NIL;
+		create->ofTypename = NULL;
+		create->constraints = NIL;
+		create->options = into->options;
+		create->oncommit = into->onCommit;
+		create->tablespacename = into->tableSpaceName;
+		create->if_not_exists = false;
+		create->accessMethod = into->accessMethod;
+
+		/*
+		 * Create the relation.  (This will error out if there's an existing
+		 * view, so we don't need more code to complain if "replace" is
+		 * false.)
+		 */
+		intoRelationAddr = DefineRelation(create, relkind, InvalidOid, NULL,
+										  NULL);
 
-	/* parse and validate reloptions for the toast table */
-	toast_options = transformRelOptions((Datum) 0,
-										create->options,
-										"toast",
-										validnsps,
-										true, false);
+		/*
+		 * If necessary, create a TOAST table for the target table.  Note that
+		 * NewRelationCreateToastTable ends with CommandCounterIncrement(), so
+		 * that the TOAST table will be visible for insertion.
+		 */
+		CommandCounterIncrement();
+
+		/* parse and validate reloptions for the toast table */
+		toast_options = transformRelOptions((Datum) 0,
+											create->options,
+											"toast",
+											validnsps,
+											true, false);
 
-	(void) heap_reloptions(RELKIND_TOASTVALUE, toast_options, true);
+		(void) heap_reloptions(RELKIND_TOASTVALUE, toast_options, true);
 
-	NewRelationCreateToastTable(intoRelationAddr.objectId, toast_options);
+		NewRelationCreateToastTable(intoRelationAddr.objectId, toast_options);
+	}
 
 	/* Create the "view" part of a materialized view. */
 	if (is_matview)
@@ -135,7 +231,7 @@ create_ctas_internal(List *attrList, IntoClause *into)
 		/* StoreViewQuery scribbles on tree, so make a copy */
 		Query	   *query = copyObject(into->viewQuery);
 
-		StoreViewQuery(intoRelationAddr.objectId, query, false);
+		StoreViewQuery(intoRelationAddr.objectId, query, replace);
 		CommandCounterIncrement();
 	}
 
@@ -232,7 +328,26 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt,
 
 	/* Check if the relation exists or not */
 	if (CreateTableAsRelExists(stmt))
+	{
+		/* An existing materialized view can be replaced. */
+		if (is_matview && into->replace)
+		{
+			RefreshMatViewStmt *refresh;
+
+			/* Change the relation to match the new query and other options. */
+			(void) create_ctas_nodata(query->targetList, into);
+
+			/* Refresh the materialized view with a fake statement. */
+			refresh = makeNode(RefreshMatViewStmt);
+			refresh->relation = into->rel;
+			refresh->skipData = into->skipData;
+			refresh->concurrent = false;
+
+			return ExecRefreshMatView(refresh, NULL, NULL);
+		}
+
 		return InvalidObjectAddress;
+	}
 
 	/*
 	 * Create the tuple receiver object and insert info it will need
@@ -400,14 +515,15 @@ CreateTableAsRelExists(CreateTableAsStmt *ctas)
 	oldrelid = get_relname_relid(into->rel->relname, nspid);
 	if (OidIsValid(oldrelid))
 	{
-		if (!ctas->if_not_exists)
+		if (!ctas->if_not_exists && !into->replace)
 			ereport(ERROR,
 					(errcode(ERRCODE_DUPLICATE_TABLE),
 					 errmsg("relation \"%s\" already exists",
 							into->rel->relname)));
 
 		/*
-		 * The relation exists and IF NOT EXISTS has been specified.
+		 * The relation exists and IF NOT EXISTS or OR REPLACE has been
+		 * specified.
 		 *
 		 * If we are in an extension script, insist that the pre-existing
 		 * object be a member of the extension, to avoid security risks.
@@ -415,11 +531,12 @@ CreateTableAsRelExists(CreateTableAsStmt *ctas)
 		ObjectAddressSet(address, RelationRelationId, oldrelid);
 		checkMembershipInCurrentExtension(&address);
 
-		/* OK to skip */
-		ereport(NOTICE,
-				(errcode(ERRCODE_DUPLICATE_TABLE),
-				 errmsg("relation \"%s\" already exists, skipping",
-						into->rel->relname)));
+		if (ctas->if_not_exists)
+			/* OK to skip */
+			ereport(NOTICE,
+					(errcode(ERRCODE_DUPLICATE_TABLE),
+					 errmsg("relation \"%s\" already exists, skipping",
+							into->rel->relname)));
 		return true;
 	}
 
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 4fc54bd6eb..12ba984a05 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -4589,7 +4589,7 @@ AlterTableGetLockLevel(List *cmds)
 				 * Subcommands that may be visible to concurrent SELECTs
 				 */
 			case AT_DropColumn: /* change visible to SELECT */
-			case AT_AddColumnToView:	/* CREATE VIEW */
+			case AT_AddColumnToView:	/* via CREATE OR REPLACE [MATERIALIZED] VIEW */
 			case AT_DropOids:	/* used to equiv to DropColumn */
 			case AT_EnableAlwaysRule:	/* may change SELECT rules */
 			case AT_EnableReplicaRule:	/* may change SELECT rules */
@@ -4884,8 +4884,8 @@ ATPrepCmd(List **wqueue, Relation rel, AlterTableCmd *cmd,
 			/* Recursion occurs during execution phase */
 			pass = AT_PASS_ADD_COL;
 			break;
-		case AT_AddColumnToView:	/* add column via CREATE OR REPLACE VIEW */
-			ATSimplePermissions(cmd->subtype, rel, ATT_VIEW);
+		case AT_AddColumnToView:	/* via CREATE OR REPLACE [MATERIALIZED] VIEW */
+			ATSimplePermissions(cmd->subtype, rel, ATT_VIEW | ATT_MATVIEW);
 			ATPrepAddColumn(wqueue, rel, recurse, recursing, true, cmd,
 							lockmode, context);
 			/* Recursion occurs during execution phase */
@@ -5314,7 +5314,7 @@ ATExecCmd(List **wqueue, AlteredTableInfo *tab,
 	switch (cmd->subtype)
 	{
 		case AT_AddColumn:		/* ADD COLUMN */
-		case AT_AddColumnToView:	/* add column via CREATE OR REPLACE VIEW */
+		case AT_AddColumnToView:	/* via CREATE OR REPLACE [MATERIALIZED] VIEW */
 			address = ATExecAddColumn(wqueue, tab, rel, &cmd,
 									  cmd->recurse, false,
 									  lockmode, cur_pass, context);
diff --git a/src/backend/commands/view.c b/src/backend/commands/view.c
index 6f0301555e..53d4cacc4c 100644
--- a/src/backend/commands/view.c
+++ b/src/backend/commands/view.c
@@ -30,8 +30,6 @@
 #include "utils/lsyscache.h"
 #include "utils/rel.h"
 
-static void checkViewColumns(TupleDesc newdesc, TupleDesc olddesc);
-
 /*---------------------------------------------------------------------
  * DefineVirtualRelation
  *
@@ -129,7 +127,7 @@ DefineVirtualRelation(RangeVar *relation, List *tlist, bool replace,
 		 * column list.
 		 */
 		descriptor = BuildDescForRelation(attrList);
-		checkViewColumns(descriptor, rel->rd_att);
+		checkViewColumns(descriptor, rel->rd_att, false);
 
 		/*
 		 * If new attributes have been added, we must add pg_attribute entries
@@ -263,15 +261,22 @@ DefineVirtualRelation(RangeVar *relation, List *tlist, bool replace,
  * added to generate specific complaints.  Also, we allow the new view to have
  * more columns than the old.
  */
-static void
-checkViewColumns(TupleDesc newdesc, TupleDesc olddesc)
+void
+checkViewColumns(TupleDesc newdesc, TupleDesc olddesc, bool is_matview)
 {
 	int			i;
 
 	if (newdesc->natts < olddesc->natts)
-		ereport(ERROR,
-				(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
-				 errmsg("cannot drop columns from view")));
+	{
+		if (is_matview)
+			ereport(ERROR,
+					errcode(ERRCODE_INVALID_TABLE_DEFINITION),
+					errmsg("cannot drop columns from materialized view"));
+		else
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
+					 errmsg("cannot drop columns from view")));
+	}
 
 	for (i = 0; i < olddesc->natts; i++)
 	{
@@ -280,17 +285,34 @@ checkViewColumns(TupleDesc newdesc, TupleDesc olddesc)
 
 		/* XXX msg not right, but we don't support DROP COL on view anyway */
 		if (newattr->attisdropped != oldattr->attisdropped)
-			ereport(ERROR,
-					(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
-					 errmsg("cannot drop columns from view")));
+		{
+			if (is_matview)
+				ereport(ERROR,
+						errcode(ERRCODE_INVALID_TABLE_DEFINITION),
+						errmsg("cannot drop columns from materialized view"));
+			else
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
+						 errmsg("cannot drop columns from view")));
+		}
 
 		if (strcmp(NameStr(newattr->attname), NameStr(oldattr->attname)) != 0)
-			ereport(ERROR,
-					(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
-					 errmsg("cannot change name of view column \"%s\" to \"%s\"",
-							NameStr(oldattr->attname),
-							NameStr(newattr->attname)),
-					 errhint("Use ALTER VIEW ... RENAME COLUMN ... to change name of view column instead.")));
+		{
+			if (is_matview)
+				ereport(ERROR,
+						errcode(ERRCODE_INVALID_TABLE_DEFINITION),
+						errmsg("cannot change name of materialized view column \"%s\" to \"%s\"",
+							   NameStr(oldattr->attname),
+							   NameStr(newattr->attname)),
+						errhint("Use ALTER MATERIALIZED VIEW ... RENAME COLUMN ... to change name of materialized view column instead."));
+			else
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
+						 errmsg("cannot change name of view column \"%s\" to \"%s\"",
+								NameStr(oldattr->attname),
+								NameStr(newattr->attname)),
+						 errhint("Use ALTER VIEW ... RENAME COLUMN ... to change name of view column instead.")));
+		}
 
 		/*
 		 * We cannot allow type, typmod, or collation to change, since these
@@ -299,26 +321,48 @@ checkViewColumns(TupleDesc newdesc, TupleDesc olddesc)
 		 */
 		if (newattr->atttypid != oldattr->atttypid ||
 			newattr->atttypmod != oldattr->atttypmod)
-			ereport(ERROR,
-					(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
-					 errmsg("cannot change data type of view column \"%s\" from %s to %s",
-							NameStr(oldattr->attname),
-							format_type_with_typemod(oldattr->atttypid,
-													 oldattr->atttypmod),
-							format_type_with_typemod(newattr->atttypid,
-													 newattr->atttypmod))));
+		{
+			if (is_matview)
+				ereport(ERROR,
+						errcode(ERRCODE_INVALID_TABLE_DEFINITION),
+						errmsg("cannot change data type of materialized view column \"%s\" from %s to %s",
+							   NameStr(oldattr->attname),
+							   format_type_with_typemod(oldattr->atttypid,
+														oldattr->atttypmod),
+							   format_type_with_typemod(newattr->atttypid,
+														newattr->atttypmod)));
+			else
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
+						 errmsg("cannot change data type of view column \"%s\" from %s to %s",
+								NameStr(oldattr->attname),
+								format_type_with_typemod(oldattr->atttypid,
+														 oldattr->atttypmod),
+								format_type_with_typemod(newattr->atttypid,
+														 newattr->atttypmod))));
+		}
 
 		/*
 		 * At this point, attcollations should be both valid or both invalid,
 		 * so applying get_collation_name unconditionally should be fine.
 		 */
 		if (newattr->attcollation != oldattr->attcollation)
-			ereport(ERROR,
-					(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
-					 errmsg("cannot change collation of view column \"%s\" from \"%s\" to \"%s\"",
-							NameStr(oldattr->attname),
-							get_collation_name(oldattr->attcollation),
-							get_collation_name(newattr->attcollation))));
+		{
+			if (is_matview)
+				ereport(ERROR,
+						errcode(ERRCODE_INVALID_TABLE_DEFINITION),
+						errmsg("cannot change collation of materialized view column \"%s\" from \"%s\" to \"%s\"",
+							   NameStr(oldattr->attname),
+							   get_collation_name(oldattr->attcollation),
+							   get_collation_name(newattr->attcollation)));
+			else
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
+						 errmsg("cannot change collation of view column \"%s\" from \"%s\" to \"%s\"",
+								NameStr(oldattr->attname),
+								get_collation_name(oldattr->attcollation),
+								get_collation_name(newattr->attcollation))));
+		}
 	}
 
 	/*
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 6079de70e0..c917c11f9e 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -4854,6 +4854,21 @@ CreateMatViewStmt:
 					$8->skipData = !($11);
 					$$ = (Node *) ctas;
 				}
+		| CREATE OR REPLACE OptNoLog MATERIALIZED VIEW create_mv_target AS SelectStmt opt_with_data
+				{
+					CreateTableAsStmt *ctas = makeNode(CreateTableAsStmt);
+
+					ctas->query = $9;
+					ctas->into = $7;
+					ctas->objtype = OBJECT_MATVIEW;
+					ctas->is_select_into = false;
+					ctas->if_not_exists = false;
+					/* cram additional flags into the IntoClause */
+					$7->rel->relpersistence = $4;
+					$7->skipData = !($10);
+					$7->replace = true;
+					$$ = (Node *) ctas;
+				}
 		;
 
 create_mv_target:
diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c
index 81cbf10aa2..dd72c617d5 100644
--- a/src/bin/psql/tab-complete.in.c
+++ b/src/bin/psql/tab-complete.in.c
@@ -2144,7 +2144,7 @@ match_previous_words(int pattern_id,
 	/* complete with something you can create or replace */
 	else if (TailMatches("CREATE", "OR", "REPLACE"))
 		COMPLETE_WITH("FUNCTION", "PROCEDURE", "LANGUAGE", "RULE", "VIEW",
-					  "AGGREGATE", "TRANSFORM", "TRIGGER");
+					  "AGGREGATE", "TRANSFORM", "TRIGGER", "MATERIALIZED VIEW");
 
 /* DROP, but not DROP embedded in other commands */
 	/* complete with something you can drop */
@@ -3979,28 +3979,34 @@ match_previous_words(int pattern_id,
 		COMPLETE_WITH("SELECT");
 
 /* CREATE MATERIALIZED VIEW */
-	else if (Matches("CREATE", "MATERIALIZED"))
+	else if (Matches("CREATE", "MATERIALIZED") ||
+			 Matches("CREATE", "OR", "REPLACE", "MATERIALIZED"))
 		COMPLETE_WITH("VIEW");
-	/* Complete CREATE MATERIALIZED VIEW <name> with AS or USING */
-	else if (Matches("CREATE", "MATERIALIZED", "VIEW", MatchAny))
+	/* Complete CREATE [ OR REPLACE ] MATERIALIZED VIEW <name> with AS or USING */
+	else if (Matches("CREATE", "MATERIALIZED", "VIEW", MatchAny) ||
+			 Matches("CREATE", "OR", "REPLACE", "MATERIALIZED", "VIEW", MatchAny))
 		COMPLETE_WITH("AS", "USING");
 
 	/*
-	 * Complete CREATE MATERIALIZED VIEW <name> USING with list of access
+	 * Complete CREATE [ OR REPLACE ] MATERIALIZED VIEW <name> USING with list of access
 	 * methods
 	 */
-	else if (Matches("CREATE", "MATERIALIZED", "VIEW", MatchAny, "USING"))
+	else if (Matches("CREATE", "MATERIALIZED", "VIEW", MatchAny, "USING") ||
+			 Matches("CREATE", "OR", "REPLACE", "MATERIALIZED", "VIEW", MatchAny, "USING"))
 		COMPLETE_WITH_QUERY(Query_for_list_of_table_access_methods);
-	/* Complete CREATE MATERIALIZED VIEW <name> USING <access method> with AS */
-	else if (Matches("CREATE", "MATERIALIZED", "VIEW", MatchAny, "USING", MatchAny))
+	/* Complete CREATE [ OR REPLACE ] MATERIALIZED VIEW <name> USING <access method> with AS */
+	else if (Matches("CREATE", "MATERIALIZED", "VIEW", MatchAny, "USING", MatchAny) ||
+			 Matches("CREATE", "OR", "REPLACE", "MATERIALIZED", "VIEW", MatchAny, "USING", MatchAny))
 		COMPLETE_WITH("AS");
 
 	/*
-	 * Complete CREATE MATERIALIZED VIEW <name> [USING <access method> ] AS
+	 * Complete CREATE [ OR REPLACE ] MATERIALIZED VIEW <name> [USING <access method> ] AS
 	 * with "SELECT"
 	 */
 	else if (Matches("CREATE", "MATERIALIZED", "VIEW", MatchAny, "AS") ||
-			 Matches("CREATE", "MATERIALIZED", "VIEW", MatchAny, "USING", MatchAny, "AS"))
+			 Matches("CREATE", "OR", "REPLACE", "MATERIALIZED", "VIEW", MatchAny, "AS") ||
+			 Matches("CREATE", "MATERIALIZED", "VIEW", MatchAny, "USING", MatchAny, "AS") ||
+			 Matches("CREATE", "OR", "REPLACE", "MATERIALIZED", "VIEW", MatchAny, "USING", MatchAny, "AS"))
 		COMPLETE_WITH("SELECT");
 
 /* CREATE EVENT TRIGGER */
diff --git a/src/include/commands/view.h b/src/include/commands/view.h
index c41f51b161..95290f0a1c 100644
--- a/src/include/commands/view.h
+++ b/src/include/commands/view.h
@@ -22,4 +22,7 @@ extern ObjectAddress DefineView(ViewStmt *stmt, const char *queryString,
 
 extern void StoreViewQuery(Oid viewOid, Query *viewParse, bool replace);
 
+extern void checkViewColumns(TupleDesc newdesc, TupleDesc olddesc,
+							 bool is_matview);
+
 #endif							/* VIEW_H */
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index b191eaaeca..f4c16e6a9e 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -2358,7 +2358,7 @@ typedef struct AlterTableStmt
 typedef enum AlterTableType
 {
 	AT_AddColumn,				/* add column */
-	AT_AddColumnToView,			/* implicitly via CREATE OR REPLACE VIEW */
+	AT_AddColumnToView,			/* implicitly via CREATE OR REPLACE [MATERIALIZED] VIEW */
 	AT_ColumnDefault,			/* alter column default */
 	AT_CookedColumnDefault,		/* add a pre-cooked column default */
 	AT_DropNotNull,				/* alter column drop not null */
diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h
index 9c2957eb54..fce057a8ac 100644
--- a/src/include/nodes/primnodes.h
+++ b/src/include/nodes/primnodes.h
@@ -168,6 +168,7 @@ typedef struct IntoClause
 	/* materialized view's SELECT query */
 	struct Query *viewQuery pg_node_attr(query_jumble_ignore);
 	bool		skipData;		/* true for WITH NO DATA */
+	bool		replace;		/* replace existing matview? */
 } IntoClause;
 
 
diff --git a/src/test/regress/expected/matview.out b/src/test/regress/expected/matview.out
index 9eab51bc2a..71f6a0681d 100644
--- a/src/test/regress/expected/matview.out
+++ b/src/test/regress/expected/matview.out
@@ -694,3 +694,194 @@ NOTICE:  relation "matview_ine_tab" already exists, skipping
 (0 rows)
 
 DROP MATERIALIZED VIEW matview_ine_tab;
+--
+-- test CREATE OR REPLACE MATERIALIZED VIEW
+--
+-- matview does not already exist
+DROP MATERIALIZED VIEW IF EXISTS mvtest_replace;
+NOTICE:  materialized view "mvtest_replace" does not exist, skipping
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 1 AS a;
+SELECT * FROM mvtest_replace;
+ a 
+---
+ 1
+(1 row)
+
+-- replace query with data
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 2 AS a;
+SELECT * FROM mvtest_replace;
+ a 
+---
+ 2
+(1 row)
+
+-- replace query without data
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 3 AS a
+  WITH NO DATA;
+SELECT * FROM mvtest_replace; -- error: not populated
+ERROR:  materialized view "mvtest_replace" has not been populated
+HINT:  Use the REFRESH MATERIALIZED VIEW command.
+REFRESH MATERIALIZED VIEW mvtest_replace;
+SELECT * FROM mvtest_replace;
+ a 
+---
+ 3
+(1 row)
+
+-- add column
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 4 AS a, 1 b;
+SELECT * FROM mvtest_replace;
+ a | b 
+---+---
+ 4 | 1
+(1 row)
+
+-- replace table options
+SELECT m.*, c.relname, c.reloptions, s.spcname, a.amname
+  FROM mvtest_replace m
+    CROSS JOIN pg_class c
+    LEFT JOIN pg_tablespace s ON s.oid = c.reltablespace
+    LEFT JOIN pg_am a ON a.oid = c.relam
+  WHERE c.relname = 'mvtest_replace';
+ a | b |    relname     | reloptions | spcname | amname 
+---+---+----------------+------------+---------+--------
+ 4 | 1 | mvtest_replace |            |         | heap
+(1 row)
+
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace
+  USING heap2
+  WITH (fillfactor = 50)
+  TABLESPACE regress_tblspace
+  AS SELECT 5 AS a, 1 AS b;
+SELECT m.*, c.relname, c.reloptions, s.spcname, a.amname
+  FROM mvtest_replace m
+    CROSS JOIN pg_class c
+    LEFT JOIN pg_tablespace s ON s.oid = c.reltablespace
+    LEFT JOIN pg_am a ON a.oid = c.relam
+  WHERE c.relname = 'mvtest_replace';
+ a | b |    relname     |   reloptions    |     spcname      | amname 
+---+---+----------------+-----------------+------------------+--------
+ 5 | 1 | mvtest_replace | {fillfactor=50} | regress_tblspace | heap2
+(1 row)
+
+-- can replace matview that has a dependent view
+CREATE VIEW mvtest_replace_v AS
+  SELECT * FROM mvtest_replace;
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 6 AS a, 1 AS b;
+SELECT * FROM mvtest_replace, mvtest_replace_v;
+ a | b | a | b 
+---+---+---+---
+ 6 | 1 | 6 | 1
+(1 row)
+
+DROP VIEW mvtest_replace_v;
+-- index gets rebuilt when replacing with data
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 7 AS a, 1 AS b;
+CREATE UNIQUE INDEX ON mvtest_replace (b);
+SELECT * FROM mvtest_replace;
+ a | b 
+---+---
+ 7 | 1
+(1 row)
+
+SET enable_seqscan = off; -- force index scan
+EXPLAIN (COSTS OFF) SELECT * FROM mvtest_replace WHERE b = 1;
+                       QUERY PLAN                        
+---------------------------------------------------------
+ Index Scan using mvtest_replace_b_idx on mvtest_replace
+   Index Cond: (b = 1)
+(2 rows)
+
+SELECT * FROM mvtest_replace WHERE b = 1;
+ a | b 
+---+---
+ 7 | 1
+(1 row)
+
+RESET enable_seqscan;
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 8 AS a, 1 AS b;
+SET enable_seqscan = off; -- force index scan
+EXPLAIN (COSTS OFF) SELECT * FROM mvtest_replace WHERE b = 1;
+                       QUERY PLAN                        
+---------------------------------------------------------
+ Index Scan using mvtest_replace_b_idx on mvtest_replace
+   Index Cond: (b = 1)
+(2 rows)
+
+SELECT * FROM mvtest_replace WHERE b = 1;
+ a | b 
+---+---
+ 8 | 1
+(1 row)
+
+RESET enable_seqscan;
+-- cannot change column data type
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 9 AS a, 'x' AS b; -- error
+ERROR:  cannot change data type of materialized view column "b" from integer to text
+SELECT * FROM mvtest_replace;
+ a | b 
+---+---
+ 8 | 1
+(1 row)
+
+-- cannot rename column
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 10 AS a, 1 AS b2; -- error
+ERROR:  cannot change name of materialized view column "b" to "b2"
+HINT:  Use ALTER MATERIALIZED VIEW ... RENAME COLUMN ... to change name of materialized view column instead.
+SELECT * FROM mvtest_replace;
+ a | b 
+---+---
+ 8 | 1
+(1 row)
+
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 11 AS a, 1 AS b, 'y' COLLATE "C" AS c;
+SELECT * FROM mvtest_replace;
+ a  | b | c 
+----+---+---
+ 11 | 1 | y
+(1 row)
+
+-- cannot change column collation
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 12 AS a, 1 AS b, 'x' COLLATE "POSIX" AS c; -- error
+ERROR:  cannot change collation of materialized view column "c" from "C" to "POSIX"
+SELECT * FROM mvtest_replace;
+ a  | b | c 
+----+---+---
+ 11 | 1 | y
+(1 row)
+
+-- cannot drop column
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 13 AS a, 1 AS b; -- error
+ERROR:  cannot drop columns from materialized view
+SELECT * FROM mvtest_replace;
+ a  | b | c 
+----+---+---
+ 11 | 1 | y
+(1 row)
+
+-- must target a matview
+CREATE VIEW mvtest_not_mv AS
+  SELECT 1 AS a;
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_not_mv AS
+  SELECT 1 AS a; -- error
+ERROR:  "mvtest_not_mv" is not a materialized view
+DROP VIEW mvtest_not_mv;
+-- cannot use OR REPLACE with IF NOT EXISTS
+CREATE OR REPLACE MATERIALIZED VIEW IF NOT EXISTS mvtest_replace AS
+  SELECT 1 AS a;
+ERROR:  syntax error at or near "NOT"
+LINE 1: CREATE OR REPLACE MATERIALIZED VIEW IF NOT EXISTS mvtest_rep...
+                                               ^
+DROP MATERIALIZED VIEW mvtest_replace;
diff --git a/src/test/regress/sql/matview.sql b/src/test/regress/sql/matview.sql
index 6704eeae2d..d6a4dc4b85 100644
--- a/src/test/regress/sql/matview.sql
+++ b/src/test/regress/sql/matview.sql
@@ -314,3 +314,111 @@ EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF)
   CREATE MATERIALIZED VIEW IF NOT EXISTS matview_ine_tab AS
     SELECT 1 / 0 WITH NO DATA; -- ok
 DROP MATERIALIZED VIEW matview_ine_tab;
+
+--
+-- test CREATE OR REPLACE MATERIALIZED VIEW
+--
+
+-- matview does not already exist
+DROP MATERIALIZED VIEW IF EXISTS mvtest_replace;
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 1 AS a;
+SELECT * FROM mvtest_replace;
+
+-- replace query with data
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 2 AS a;
+SELECT * FROM mvtest_replace;
+
+-- replace query without data
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 3 AS a
+  WITH NO DATA;
+SELECT * FROM mvtest_replace; -- error: not populated
+REFRESH MATERIALIZED VIEW mvtest_replace;
+SELECT * FROM mvtest_replace;
+
+-- add column
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 4 AS a, 1 b;
+SELECT * FROM mvtest_replace;
+
+-- replace table options
+SELECT m.*, c.relname, c.reloptions, s.spcname, a.amname
+  FROM mvtest_replace m
+    CROSS JOIN pg_class c
+    LEFT JOIN pg_tablespace s ON s.oid = c.reltablespace
+    LEFT JOIN pg_am a ON a.oid = c.relam
+  WHERE c.relname = 'mvtest_replace';
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace
+  USING heap2
+  WITH (fillfactor = 50)
+  TABLESPACE regress_tblspace
+  AS SELECT 5 AS a, 1 AS b;
+SELECT m.*, c.relname, c.reloptions, s.spcname, a.amname
+  FROM mvtest_replace m
+    CROSS JOIN pg_class c
+    LEFT JOIN pg_tablespace s ON s.oid = c.reltablespace
+    LEFT JOIN pg_am a ON a.oid = c.relam
+  WHERE c.relname = 'mvtest_replace';
+
+-- can replace matview that has a dependent view
+CREATE VIEW mvtest_replace_v AS
+  SELECT * FROM mvtest_replace;
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 6 AS a, 1 AS b;
+SELECT * FROM mvtest_replace, mvtest_replace_v;
+DROP VIEW mvtest_replace_v;
+
+-- index gets rebuilt when replacing with data
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 7 AS a, 1 AS b;
+CREATE UNIQUE INDEX ON mvtest_replace (b);
+SELECT * FROM mvtest_replace;
+SET enable_seqscan = off; -- force index scan
+EXPLAIN (COSTS OFF) SELECT * FROM mvtest_replace WHERE b = 1;
+SELECT * FROM mvtest_replace WHERE b = 1;
+RESET enable_seqscan;
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 8 AS a, 1 AS b;
+SET enable_seqscan = off; -- force index scan
+EXPLAIN (COSTS OFF) SELECT * FROM mvtest_replace WHERE b = 1;
+SELECT * FROM mvtest_replace WHERE b = 1;
+RESET enable_seqscan;
+
+-- cannot change column data type
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 9 AS a, 'x' AS b; -- error
+SELECT * FROM mvtest_replace;
+
+-- cannot rename column
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 10 AS a, 1 AS b2; -- error
+SELECT * FROM mvtest_replace;
+
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 11 AS a, 1 AS b, 'y' COLLATE "C" AS c;
+SELECT * FROM mvtest_replace;
+
+-- cannot change column collation
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 12 AS a, 1 AS b, 'x' COLLATE "POSIX" AS c; -- error
+SELECT * FROM mvtest_replace;
+
+-- cannot drop column
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 13 AS a, 1 AS b; -- error
+SELECT * FROM mvtest_replace;
+
+-- must target a matview
+CREATE VIEW mvtest_not_mv AS
+  SELECT 1 AS a;
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_not_mv AS
+  SELECT 1 AS a; -- error
+DROP VIEW mvtest_not_mv;
+
+-- cannot use OR REPLACE with IF NOT EXISTS
+CREATE OR REPLACE MATERIALIZED VIEW IF NOT EXISTS mvtest_replace AS
+  SELECT 1 AS a;
+
+DROP MATERIALIZED VIEW mvtest_replace;
-- 
2.48.0
From 2478cd438360a2fd5d74082c96b8feabd49ac593 Mon Sep 17 00:00:00 2001
From: Erik Wienhold <ewie@ewie.name>
Date: Tue, 28 May 2024 02:19:53 +0200
Subject: [PATCH v5 2/3] Deprecate CREATE MATERIALIZED VIEW IF NOT EXISTS

---
 src/backend/parser/gram.y                     | 14 ++++++
 .../expected/test_extensions.out              | 45 +++++++++++++++++++
 src/test/regress/expected/matview.out         | 20 +++++++++
 3 files changed, 79 insertions(+)

diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index c917c11f9e..cb5647dcb6 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -4853,6 +4853,20 @@ CreateMatViewStmt:
 					$8->rel->relpersistence = $2;
 					$8->skipData = !($11);
 					$$ = (Node *) ctas;
+
+					if (ctas->into->rel->schemaname)
+							ereport(WARNING,
+									errmsg("IF NOT EXISTS is deprecated in materialized view creation"),
+									errhint("Use CREATE OR REPLACE MATERIALIZED VIEW %s.%s.",
+											ctas->into->rel->schemaname,
+											ctas->into->rel->relname),
+									parser_errposition(@1));
+					else
+							ereport(WARNING,
+									errmsg("IF NOT EXISTS is deprecated in materialized view creation"),
+									errhint("Use CREATE OR REPLACE MATERIALIZED VIEW %s.",
+											ctas->into->rel->relname),
+									parser_errposition(@1));
 				}
 		| CREATE OR REPLACE OptNoLog MATERIALIZED VIEW create_mv_target AS SelectStmt opt_with_data
 				{
diff --git a/src/test/modules/test_extensions/expected/test_extensions.out b/src/test/modules/test_extensions/expected/test_extensions.out
index d5388a1fec..9a1e0f7658 100644
--- a/src/test/modules/test_extensions/expected/test_extensions.out
+++ b/src/test/modules/test_extensions/expected/test_extensions.out
@@ -404,6 +404,11 @@ Objects in extension "test_ext_cor"
 CREATE COLLATION ext_cine_coll
   ( LC_COLLATE = "C", LC_CTYPE = "C" );
 CREATE EXTENSION test_ext_cine;  -- fail
+WARNING:  IF NOT EXISTS is deprecated in materialized view creation
+LINE 1: CREATE MATERIALIZED VIEW IF NOT EXISTS ext_cine_mv AS SELECT...
+        ^
+HINT:  Use CREATE OR REPLACE MATERIALIZED VIEW ext_cine_mv.
+QUERY:  CREATE MATERIALIZED VIEW IF NOT EXISTS ext_cine_mv AS SELECT 42 AS f1;
 ERROR:  collation ext_cine_coll is not a member of extension "test_ext_cine"
 DETAIL:  An extension may only use CREATE ... IF NOT EXISTS to skip object creation if the conflicting object is one that it already owns.
 CONTEXT:  SQL statement "CREATE COLLATION IF NOT EXISTS ext_cine_coll
@@ -412,6 +417,11 @@ extension script file "test_ext_cine--1.0.sql", near line 10
 DROP COLLATION ext_cine_coll;
 CREATE MATERIALIZED VIEW ext_cine_mv AS SELECT 11 AS f1;
 CREATE EXTENSION test_ext_cine;  -- fail
+WARNING:  IF NOT EXISTS is deprecated in materialized view creation
+LINE 1: CREATE MATERIALIZED VIEW IF NOT EXISTS ext_cine_mv AS SELECT...
+        ^
+HINT:  Use CREATE OR REPLACE MATERIALIZED VIEW ext_cine_mv.
+QUERY:  CREATE MATERIALIZED VIEW IF NOT EXISTS ext_cine_mv AS SELECT 42 AS f1;
 ERROR:  materialized view ext_cine_mv is not a member of extension "test_ext_cine"
 DETAIL:  An extension may only use CREATE ... IF NOT EXISTS to skip object creation if the conflicting object is one that it already owns.
 CONTEXT:  SQL statement "CREATE MATERIALIZED VIEW IF NOT EXISTS ext_cine_mv AS SELECT 42 AS f1"
@@ -420,6 +430,11 @@ DROP MATERIALIZED VIEW ext_cine_mv;
 CREATE FOREIGN DATA WRAPPER dummy;
 CREATE SERVER ext_cine_srv FOREIGN DATA WRAPPER dummy;
 CREATE EXTENSION test_ext_cine;  -- fail
+WARNING:  IF NOT EXISTS is deprecated in materialized view creation
+LINE 1: CREATE MATERIALIZED VIEW IF NOT EXISTS ext_cine_mv AS SELECT...
+        ^
+HINT:  Use CREATE OR REPLACE MATERIALIZED VIEW ext_cine_mv.
+QUERY:  CREATE MATERIALIZED VIEW IF NOT EXISTS ext_cine_mv AS SELECT 42 AS f1;
 ERROR:  server ext_cine_srv is not a member of extension "test_ext_cine"
 DETAIL:  An extension may only use CREATE ... IF NOT EXISTS to skip object creation if the conflicting object is one that it already owns.
 CONTEXT:  SQL statement "CREATE SERVER IF NOT EXISTS ext_cine_srv FOREIGN DATA WRAPPER ext_cine_fdw"
@@ -427,6 +442,11 @@ extension script file "test_ext_cine--1.0.sql", near line 17
 DROP SERVER ext_cine_srv;
 CREATE SCHEMA ext_cine_schema;
 CREATE EXTENSION test_ext_cine;  -- fail
+WARNING:  IF NOT EXISTS is deprecated in materialized view creation
+LINE 1: CREATE MATERIALIZED VIEW IF NOT EXISTS ext_cine_mv AS SELECT...
+        ^
+HINT:  Use CREATE OR REPLACE MATERIALIZED VIEW ext_cine_mv.
+QUERY:  CREATE MATERIALIZED VIEW IF NOT EXISTS ext_cine_mv AS SELECT 42 AS f1;
 ERROR:  schema ext_cine_schema is not a member of extension "test_ext_cine"
 DETAIL:  An extension may only use CREATE ... IF NOT EXISTS to skip object creation if the conflicting object is one that it already owns.
 CONTEXT:  SQL statement "CREATE SCHEMA IF NOT EXISTS ext_cine_schema"
@@ -434,6 +454,11 @@ extension script file "test_ext_cine--1.0.sql", near line 19
 DROP SCHEMA ext_cine_schema;
 CREATE SEQUENCE ext_cine_seq;
 CREATE EXTENSION test_ext_cine;  -- fail
+WARNING:  IF NOT EXISTS is deprecated in materialized view creation
+LINE 1: CREATE MATERIALIZED VIEW IF NOT EXISTS ext_cine_mv AS SELECT...
+        ^
+HINT:  Use CREATE OR REPLACE MATERIALIZED VIEW ext_cine_mv.
+QUERY:  CREATE MATERIALIZED VIEW IF NOT EXISTS ext_cine_mv AS SELECT 42 AS f1;
 ERROR:  sequence ext_cine_seq is not a member of extension "test_ext_cine"
 DETAIL:  An extension may only use CREATE ... IF NOT EXISTS to skip object creation if the conflicting object is one that it already owns.
 CONTEXT:  SQL statement "CREATE SEQUENCE IF NOT EXISTS ext_cine_seq"
@@ -441,6 +466,11 @@ extension script file "test_ext_cine--1.0.sql", near line 21
 DROP SEQUENCE ext_cine_seq;
 CREATE TABLE ext_cine_tab1 (x int);
 CREATE EXTENSION test_ext_cine;  -- fail
+WARNING:  IF NOT EXISTS is deprecated in materialized view creation
+LINE 1: CREATE MATERIALIZED VIEW IF NOT EXISTS ext_cine_mv AS SELECT...
+        ^
+HINT:  Use CREATE OR REPLACE MATERIALIZED VIEW ext_cine_mv.
+QUERY:  CREATE MATERIALIZED VIEW IF NOT EXISTS ext_cine_mv AS SELECT 42 AS f1;
 ERROR:  table ext_cine_tab1 is not a member of extension "test_ext_cine"
 DETAIL:  An extension may only use CREATE ... IF NOT EXISTS to skip object creation if the conflicting object is one that it already owns.
 CONTEXT:  SQL statement "CREATE TABLE IF NOT EXISTS ext_cine_tab1 (x int)"
@@ -448,12 +478,22 @@ extension script file "test_ext_cine--1.0.sql", near line 23
 DROP TABLE ext_cine_tab1;
 CREATE TABLE ext_cine_tab2 AS SELECT 42 AS y;
 CREATE EXTENSION test_ext_cine;  -- fail
+WARNING:  IF NOT EXISTS is deprecated in materialized view creation
+LINE 1: CREATE MATERIALIZED VIEW IF NOT EXISTS ext_cine_mv AS SELECT...
+        ^
+HINT:  Use CREATE OR REPLACE MATERIALIZED VIEW ext_cine_mv.
+QUERY:  CREATE MATERIALIZED VIEW IF NOT EXISTS ext_cine_mv AS SELECT 42 AS f1;
 ERROR:  table ext_cine_tab2 is not a member of extension "test_ext_cine"
 DETAIL:  An extension may only use CREATE ... IF NOT EXISTS to skip object creation if the conflicting object is one that it already owns.
 CONTEXT:  SQL statement "CREATE TABLE IF NOT EXISTS ext_cine_tab2 AS SELECT 42 AS y"
 extension script file "test_ext_cine--1.0.sql", near line 25
 DROP TABLE ext_cine_tab2;
 CREATE EXTENSION test_ext_cine;
+WARNING:  IF NOT EXISTS is deprecated in materialized view creation
+LINE 1: CREATE MATERIALIZED VIEW IF NOT EXISTS ext_cine_mv AS SELECT...
+        ^
+HINT:  Use CREATE OR REPLACE MATERIALIZED VIEW ext_cine_mv.
+QUERY:  CREATE MATERIALIZED VIEW IF NOT EXISTS ext_cine_mv AS SELECT 42 AS f1;
 \dx+ test_ext_cine
 Objects in extension "test_ext_cine"
         Object description         
@@ -475,6 +515,11 @@ Objects in extension "test_ext_cine"
 (14 rows)
 
 ALTER EXTENSION test_ext_cine UPDATE TO '1.1';
+WARNING:  IF NOT EXISTS is deprecated in materialized view creation
+LINE 1: CREATE MATERIALIZED VIEW IF NOT EXISTS ext_cine_mv AS SELECT...
+        ^
+HINT:  Use CREATE OR REPLACE MATERIALIZED VIEW ext_cine_mv.
+QUERY:  CREATE MATERIALIZED VIEW IF NOT EXISTS ext_cine_mv AS SELECT 42 AS f1;
 \dx+ test_ext_cine
 Objects in extension "test_ext_cine"
         Object description         
diff --git a/src/test/regress/expected/matview.out b/src/test/regress/expected/matview.out
index 71f6a0681d..d506c615da 100644
--- a/src/test/regress/expected/matview.out
+++ b/src/test/regress/expected/matview.out
@@ -565,6 +565,10 @@ CREATE MATERIALIZED VIEW mvtest_mv_foo AS SELECT * FROM mvtest_foo_data;
 CREATE MATERIALIZED VIEW mvtest_mv_foo AS SELECT * FROM mvtest_foo_data;
 ERROR:  relation "mvtest_mv_foo" already exists
 CREATE MATERIALIZED VIEW IF NOT EXISTS mvtest_mv_foo AS SELECT * FROM mvtest_foo_data;
+WARNING:  IF NOT EXISTS is deprecated in materialized view creation
+LINE 1: CREATE MATERIALIZED VIEW IF NOT EXISTS mvtest_mv_foo AS SELE...
+        ^
+HINT:  Use CREATE OR REPLACE MATERIALIZED VIEW mvtest_mv_foo.
 NOTICE:  relation "mvtest_mv_foo" already exists, skipping
 CREATE UNIQUE INDEX ON mvtest_mv_foo (i);
 RESET ROLE;
@@ -662,12 +666,20 @@ CREATE MATERIALIZED VIEW matview_ine_tab AS SELECT 1 / 0; -- error
 ERROR:  relation "matview_ine_tab" already exists
 CREATE MATERIALIZED VIEW IF NOT EXISTS matview_ine_tab AS
   SELECT 1 / 0; -- ok
+WARNING:  IF NOT EXISTS is deprecated in materialized view creation
+LINE 1: CREATE MATERIALIZED VIEW IF NOT EXISTS matview_ine_tab AS
+        ^
+HINT:  Use CREATE OR REPLACE MATERIALIZED VIEW matview_ine_tab.
 NOTICE:  relation "matview_ine_tab" already exists, skipping
 CREATE MATERIALIZED VIEW matview_ine_tab AS
   SELECT 1 / 0 WITH NO DATA; -- error
 ERROR:  relation "matview_ine_tab" already exists
 CREATE MATERIALIZED VIEW IF NOT EXISTS matview_ine_tab AS
   SELECT 1 / 0 WITH NO DATA; -- ok
+WARNING:  IF NOT EXISTS is deprecated in materialized view creation
+LINE 1: CREATE MATERIALIZED VIEW IF NOT EXISTS matview_ine_tab AS
+        ^
+HINT:  Use CREATE OR REPLACE MATERIALIZED VIEW matview_ine_tab.
 NOTICE:  relation "matview_ine_tab" already exists, skipping
 EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF)
   CREATE MATERIALIZED VIEW matview_ine_tab AS
@@ -676,6 +688,10 @@ ERROR:  relation "matview_ine_tab" already exists
 EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF)
   CREATE MATERIALIZED VIEW IF NOT EXISTS matview_ine_tab AS
     SELECT 1 / 0; -- ok
+WARNING:  IF NOT EXISTS is deprecated in materialized view creation
+LINE 2:   CREATE MATERIALIZED VIEW IF NOT EXISTS matview_ine_tab AS
+          ^
+HINT:  Use CREATE OR REPLACE MATERIALIZED VIEW matview_ine_tab.
 NOTICE:  relation "matview_ine_tab" already exists, skipping
  QUERY PLAN 
 ------------
@@ -688,6 +704,10 @@ ERROR:  relation "matview_ine_tab" already exists
 EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF)
   CREATE MATERIALIZED VIEW IF NOT EXISTS matview_ine_tab AS
     SELECT 1 / 0 WITH NO DATA; -- ok
+WARNING:  IF NOT EXISTS is deprecated in materialized view creation
+LINE 2:   CREATE MATERIALIZED VIEW IF NOT EXISTS matview_ine_tab AS
+          ^
+HINT:  Use CREATE OR REPLACE MATERIALIZED VIEW matview_ine_tab.
 NOTICE:  relation "matview_ine_tab" already exists, skipping
  QUERY PLAN 
 ------------
-- 
2.48.0
From 68c8209174f48479df34f37b9ad303a737571473 Mon Sep 17 00:00:00 2001
From: Erik Wienhold <ewie@ewie.name>
Date: Fri, 26 Jul 2024 23:33:15 +0200
Subject: [PATCH v5 3/3] Replace matview WITH OLD DATA

---
 .../sgml/ref/create_materialized_view.sgml    | 16 +++++++++--
 src/backend/commands/createas.c               | 26 +++++++++++------
 src/backend/parser/gram.y                     | 16 +++++++++++
 src/include/nodes/primnodes.h                 |  1 +
 src/test/regress/expected/matview.out         | 28 +++++++++++++++++++
 src/test/regress/sql/matview.sql              | 15 ++++++++++
 6 files changed, 90 insertions(+), 12 deletions(-)

diff --git a/doc/src/sgml/ref/create_materialized_view.sgml b/doc/src/sgml/ref/create_materialized_view.sgml
index 5e03320eb7..1352e9de40 100644
--- a/doc/src/sgml/ref/create_materialized_view.sgml
+++ b/doc/src/sgml/ref/create_materialized_view.sgml
@@ -27,7 +27,7 @@ CREATE [ OR REPLACE ] MATERIALIZED VIEW [ IF NOT EXISTS ] <replaceable>table_nam
     [ WITH ( <replaceable class="parameter">storage_parameter</replaceable> [= <replaceable class="parameter">value</replaceable>] [, ... ] ) ]
     [ TABLESPACE <replaceable class="parameter">tablespace_name</replaceable> ]
     AS <replaceable>query</replaceable>
-    [ WITH [ NO ] DATA ]
+    [ WITH [ NO | OLD ] DATA ]
 </synopsis>
  </refsynopsisdiv>
 
@@ -37,7 +37,8 @@ CREATE [ OR REPLACE ] MATERIALIZED VIEW [ IF NOT EXISTS ] <replaceable>table_nam
   <para>
    <command>CREATE MATERIALIZED VIEW</command> defines a materialized view of
    a query.  The query is executed and used to populate the view at the time
-   the command is issued (unless <command>WITH NO DATA</command> is used) and may be
+   the command is issued (unless <command>WITH NO DATA</command> or
+   <command>WITH OLD DATA</command> is used) and may be
    refreshed later using <command>REFRESH MATERIALIZED VIEW</command>.
   </para>
 
@@ -162,7 +163,7 @@ CREATE [ OR REPLACE ] MATERIALIZED VIEW [ IF NOT EXISTS ] <replaceable>table_nam
    </varlistentry>
 
    <varlistentry>
-    <term><literal>WITH [ NO ] DATA</literal></term>
+    <term><literal>WITH [ NO | OLD ] DATA</literal></term>
     <listitem>
      <para>
       This clause specifies whether or not the materialized view should be
@@ -170,6 +171,15 @@ CREATE [ OR REPLACE ] MATERIALIZED VIEW [ IF NOT EXISTS ] <replaceable>table_nam
       flagged as unscannable and cannot be queried until <command>REFRESH
       MATERIALIZED VIEW</command> is used.
      </para>
+
+     <para>
+      The form <command>WITH OLD DATA</command> keeps the already stored data
+      when replacing an existing materialized view to keep it populated.  For
+      newly created materialized views, this has the same effect as
+      <command>WITH DATA</command>.  Use this form if you want to use
+      <command>REFRESH MATERIALIZED VIEW CONCURRENTLY</command> as it requires
+      a populated materialized view.
+     </para>
     </listitem>
    </varlistentry>
 
diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c
index cba369114b..1af714fe4a 100644
--- a/src/backend/commands/createas.c
+++ b/src/backend/commands/createas.c
@@ -332,18 +332,26 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt,
 		/* An existing materialized view can be replaced. */
 		if (is_matview && into->replace)
 		{
-			RefreshMatViewStmt *refresh;
-
 			/* Change the relation to match the new query and other options. */
-			(void) create_ctas_nodata(query->targetList, into);
+			address = create_ctas_nodata(query->targetList, into);
 
-			/* Refresh the materialized view with a fake statement. */
-			refresh = makeNode(RefreshMatViewStmt);
-			refresh->relation = into->rel;
-			refresh->skipData = into->skipData;
-			refresh->concurrent = false;
+			/*
+			 * Refresh the materialized view with a fake statement unless we
+			 * must keep the old data.
+			 */
+			if (!into->keepData)
+			{
+				RefreshMatViewStmt *refresh;
+
+				refresh = makeNode(RefreshMatViewStmt);
+				refresh->relation = into->rel;
+				refresh->skipData = into->skipData;
+				refresh->concurrent = false;
+
+				address = ExecRefreshMatView(refresh, NULL, NULL);
+			}
 
-			return ExecRefreshMatView(refresh, NULL, NULL);
+			return address;
 		}
 
 		return InvalidObjectAddress;
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index cb5647dcb6..9e7041f98e 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -4883,6 +4883,22 @@ CreateMatViewStmt:
 					$7->replace = true;
 					$$ = (Node *) ctas;
 				}
+		| CREATE OR REPLACE OptNoLog MATERIALIZED VIEW create_mv_target AS SelectStmt WITH OLD DATA_P
+				{
+					CreateTableAsStmt *ctas = makeNode(CreateTableAsStmt);
+
+					ctas->query = $9;
+					ctas->into = $7;
+					ctas->objtype = OBJECT_MATVIEW;
+					ctas->is_select_into = false;
+					ctas->if_not_exists = false;
+					/* cram additional flags into the IntoClause */
+					$7->rel->relpersistence = $4;
+					$7->skipData = false;
+					$7->keepData = true;
+					$7->replace = true;
+					$$ = (Node *) ctas;
+				}
 		;
 
 create_mv_target:
diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h
index fce057a8ac..793c971133 100644
--- a/src/include/nodes/primnodes.h
+++ b/src/include/nodes/primnodes.h
@@ -168,6 +168,7 @@ typedef struct IntoClause
 	/* materialized view's SELECT query */
 	struct Query *viewQuery pg_node_attr(query_jumble_ignore);
 	bool		skipData;		/* true for WITH NO DATA */
+	bool		keepData;		/* true for WITH OLD DATA */
 	bool		replace;		/* replace existing matview? */
 } IntoClause;
 
diff --git a/src/test/regress/expected/matview.out b/src/test/regress/expected/matview.out
index d506c615da..04c2095c74 100644
--- a/src/test/regress/expected/matview.out
+++ b/src/test/regress/expected/matview.out
@@ -751,6 +751,23 @@ SELECT * FROM mvtest_replace;
  3
 (1 row)
 
+-- replace query but keep old data
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 5 AS a
+  WITH OLD DATA;
+SELECT * FROM mvtest_replace;
+ a 
+---
+ 3
+(1 row)
+
+REFRESH MATERIALIZED VIEW mvtest_replace;
+SELECT * FROM mvtest_replace;
+ a 
+---
+ 5
+(1 row)
+
 -- add column
 CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
   SELECT 4 AS a, 1 b;
@@ -905,3 +922,14 @@ ERROR:  syntax error at or near "NOT"
 LINE 1: CREATE OR REPLACE MATERIALIZED VIEW IF NOT EXISTS mvtest_rep...
                                                ^
 DROP MATERIALIZED VIEW mvtest_replace;
+-- Create new matview WITH OLD DATA.  This populates the new matview as if
+-- WITH DATA had been specified.
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 17 AS a
+  WITH OLD DATA;
+SELECT * FROM mvtest_replace;
+ a  
+----
+ 17
+(1 row)
+
diff --git a/src/test/regress/sql/matview.sql b/src/test/regress/sql/matview.sql
index d6a4dc4b85..91f547e9cb 100644
--- a/src/test/regress/sql/matview.sql
+++ b/src/test/regress/sql/matview.sql
@@ -338,6 +338,14 @@ SELECT * FROM mvtest_replace; -- error: not populated
 REFRESH MATERIALIZED VIEW mvtest_replace;
 SELECT * FROM mvtest_replace;
 
+-- replace query but keep old data
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 5 AS a
+  WITH OLD DATA;
+SELECT * FROM mvtest_replace;
+REFRESH MATERIALIZED VIEW mvtest_replace;
+SELECT * FROM mvtest_replace;
+
 -- add column
 CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
   SELECT 4 AS a, 1 b;
@@ -422,3 +430,10 @@ CREATE OR REPLACE MATERIALIZED VIEW IF NOT EXISTS mvtest_replace AS
   SELECT 1 AS a;
 
 DROP MATERIALIZED VIEW mvtest_replace;
+
+-- Create new matview WITH OLD DATA.  This populates the new matview as if
+-- WITH DATA had been specified.
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 17 AS a
+  WITH OLD DATA;
+SELECT * FROM mvtest_replace;
-- 
2.48.0

Attachments:

  [text/plain] v5-0001-Add-CREATE-OR-REPLACE-MATERIALIZED-VIEW.patch (33.0K, ../../64495e31-0a91-4894-87f8-31451ee69f44@ewie.name/2-v5-0001-Add-CREATE-OR-REPLACE-MATERIALIZED-VIEW.patch)
  download | inline diff:
From ac9bba0960f7a6fa507020400f1b4bcf4c9a25d3 Mon Sep 17 00:00:00 2001
From: Erik Wienhold <ewie@ewie.name>
Date: Tue, 21 May 2024 18:35:47 +0200
Subject: [PATCH v5 1/3] Add CREATE OR REPLACE MATERIALIZED VIEW

---
 .../sgml/ref/create_materialized_view.sgml    |  15 +-
 src/backend/commands/createas.c               | 207 ++++++++++++++----
 src/backend/commands/tablecmds.c              |   8 +-
 src/backend/commands/view.c                   | 106 ++++++---
 src/backend/parser/gram.y                     |  15 ++
 src/bin/psql/tab-complete.in.c                |  26 ++-
 src/include/commands/view.h                   |   3 +
 src/include/nodes/parsenodes.h                |   2 +-
 src/include/nodes/primnodes.h                 |   1 +
 src/test/regress/expected/matview.out         | 191 ++++++++++++++++
 src/test/regress/sql/matview.sql              | 108 +++++++++
 11 files changed, 589 insertions(+), 93 deletions(-)

diff --git a/doc/src/sgml/ref/create_materialized_view.sgml b/doc/src/sgml/ref/create_materialized_view.sgml
index 62d897931c..5e03320eb7 100644
--- a/doc/src/sgml/ref/create_materialized_view.sgml
+++ b/doc/src/sgml/ref/create_materialized_view.sgml
@@ -21,7 +21,7 @@ PostgreSQL documentation
 
  <refsynopsisdiv>
 <synopsis>
-CREATE MATERIALIZED VIEW [ IF NOT EXISTS ] <replaceable>table_name</replaceable>
+CREATE [ OR REPLACE ] MATERIALIZED VIEW [ IF NOT EXISTS ] <replaceable>table_name</replaceable>
     [ (<replaceable>column_name</replaceable> [, ...] ) ]
     [ USING <replaceable class="parameter">method</replaceable> ]
     [ WITH ( <replaceable class="parameter">storage_parameter</replaceable> [= <replaceable class="parameter">value</replaceable>] [, ... ] ) ]
@@ -60,6 +60,17 @@ CREATE MATERIALIZED VIEW [ IF NOT EXISTS ] <replaceable>table_name</replaceable>
   <title>Parameters</title>
 
   <variablelist>
+   <varlistentry>
+    <term><literal>OR REPLACE</literal></term>
+    <listitem>
+     <para>
+      Replaces a materialized view if it already exists.
+      Specifying <literal>OR REPLACE</literal> together with
+      <literal>IF NOT EXISTS</literal> is an error.
+     </para>
+    </listitem>
+   </varlistentry>
+
    <varlistentry>
     <term><literal>IF NOT EXISTS</literal></term>
     <listitem>
@@ -67,7 +78,7 @@ CREATE MATERIALIZED VIEW [ IF NOT EXISTS ] <replaceable>table_name</replaceable>
       Do not throw an error if a materialized view with the same name already
       exists. A notice is issued in this case.  Note that there is no guarantee
       that the existing materialized view is anything like the one that would
-      have been created.
+      have been created, unless you use <literal>OR REPLACE</literal> instead.
      </para>
     </listitem>
    </varlistentry>
diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c
index 23cecd99c9..cba369114b 100644
--- a/src/backend/commands/createas.c
+++ b/src/backend/commands/createas.c
@@ -79,55 +79,151 @@ static void intorel_destroy(DestReceiver *self);
 static ObjectAddress
 create_ctas_internal(List *attrList, IntoClause *into)
 {
-	CreateStmt *create = makeNode(CreateStmt);
-	bool		is_matview;
+	bool		is_matview,
+				replace = false;
 	char		relkind;
-	Datum		toast_options;
-	const char *const validnsps[] = HEAP_RELOPT_NAMESPACES;
+	Oid			matviewOid = InvalidOid;
 	ObjectAddress intoRelationAddr;
 
 	/* This code supports both CREATE TABLE AS and CREATE MATERIALIZED VIEW */
 	is_matview = (into->viewQuery != NULL);
 	relkind = is_matview ? RELKIND_MATVIEW : RELKIND_RELATION;
 
-	/*
-	 * Create the target relation by faking up a CREATE TABLE parsetree and
-	 * passing it to DefineRelation.
-	 */
-	create->relation = into->rel;
-	create->tableElts = attrList;
-	create->inhRelations = NIL;
-	create->ofTypename = NULL;
-	create->constraints = NIL;
-	create->options = into->options;
-	create->oncommit = into->onCommit;
-	create->tablespacename = into->tableSpaceName;
-	create->if_not_exists = false;
-	create->accessMethod = into->accessMethod;
+	/* Check if an existing materialized view needs to be replaced. */
+	if (is_matview)
+	{
+		LOCKMODE	lockmode;
 
-	/*
-	 * Create the relation.  (This will error out if there's an existing view,
-	 * so we don't need more code to complain if "replace" is false.)
-	 */
-	intoRelationAddr = DefineRelation(create, relkind, InvalidOid, NULL, NULL);
+		lockmode = into->replace ? AccessExclusiveLock : NoLock;
+		(void) RangeVarGetAndCheckCreationNamespace(into->rel, lockmode,
+													&matviewOid);
+		replace = OidIsValid(matviewOid) && into->replace;
+	}
 
-	/*
-	 * If necessary, create a TOAST table for the target table.  Note that
-	 * NewRelationCreateToastTable ends with CommandCounterIncrement(), so
-	 * that the TOAST table will be visible for insertion.
-	 */
-	CommandCounterIncrement();
+	if (is_matview && replace)
+	{
+		Relation	rel;
+		List	   *atcmds = NIL;
+		AlterTableCmd *atcmd;
+		TupleDesc	descriptor;
+
+		rel = relation_open(matviewOid, NoLock);
+
+		if (rel->rd_rel->relkind != RELKIND_MATVIEW)
+			ereport(ERROR,
+					errcode(ERRCODE_WRONG_OBJECT_TYPE),
+					errmsg("\"%s\" is not a materialized view",
+						   RelationGetRelationName(rel)));
+
+		CheckTableNotInUse(rel, "CREATE OR REPLACE MATERIALIZED VIEW");
+
+		descriptor = BuildDescForRelation(attrList);
+		checkViewColumns(descriptor, rel->rd_att, true);
+
+		/* Add new attributes via ALTER TABLE. */
+		if (list_length(attrList) > rel->rd_att->natts)
+		{
+			ListCell   *c;
+			int			skip = rel->rd_att->natts;
+
+			foreach(c, attrList)
+			{
+				if (skip > 0)
+				{
+					skip--;
+					continue;
+				}
+				atcmd = makeNode(AlterTableCmd);
+				atcmd->subtype = AT_AddColumnToView;
+				atcmd->def = (Node *) lfirst(c);
+				atcmds = lappend(atcmds, atcmd);
+			}
+		}
+
+		/* Set access method via ALTER TABLE. */
+		if (into->accessMethod != NULL)
+		{
+			atcmd = makeNode(AlterTableCmd);
+			atcmd->subtype = AT_SetAccessMethod;
+			atcmd->name = into->accessMethod;
+			atcmds = lappend(atcmds, atcmd);
+		}
+
+		/* Set tablespace via ALTER TABLE. */
+		if (into->tableSpaceName != NULL)
+		{
+			atcmd = makeNode(AlterTableCmd);
+			atcmd->subtype = AT_SetTableSpace;
+			atcmd->name = into->tableSpaceName;
+			atcmds = lappend(atcmds, atcmd);
+		}
+
+		/* Set storage parameters via ALTER TABLE. */
+		if (into->options != NIL)
+		{
+			atcmd = makeNode(AlterTableCmd);
+			atcmd->subtype = AT_ReplaceRelOptions;
+			atcmd->def = (Node *) into->options;
+			atcmds = lappend(atcmds, atcmd);
+		}
+
+		if (atcmds != NIL)
+		{
+			AlterTableInternal(matviewOid, atcmds, true);
+			CommandCounterIncrement();
+		}
+
+		relation_close(rel, NoLock);
+
+		ObjectAddressSet(intoRelationAddr, RelationRelationId, matviewOid);
+	}
+	else
+	{
+		CreateStmt *create = makeNode(CreateStmt);
+		Datum		toast_options;
+		const static char *validnsps[] = HEAP_RELOPT_NAMESPACES;
+
+		/*
+		 * Create the target relation by faking up a CREATE TABLE parsetree
+		 * and passing it to DefineRelation.
+		 */
+		create->relation = into->rel;
+		create->tableElts = attrList;
+		create->inhRelations = NIL;
+		create->ofTypename = NULL;
+		create->constraints = NIL;
+		create->options = into->options;
+		create->oncommit = into->onCommit;
+		create->tablespacename = into->tableSpaceName;
+		create->if_not_exists = false;
+		create->accessMethod = into->accessMethod;
+
+		/*
+		 * Create the relation.  (This will error out if there's an existing
+		 * view, so we don't need more code to complain if "replace" is
+		 * false.)
+		 */
+		intoRelationAddr = DefineRelation(create, relkind, InvalidOid, NULL,
+										  NULL);
 
-	/* parse and validate reloptions for the toast table */
-	toast_options = transformRelOptions((Datum) 0,
-										create->options,
-										"toast",
-										validnsps,
-										true, false);
+		/*
+		 * If necessary, create a TOAST table for the target table.  Note that
+		 * NewRelationCreateToastTable ends with CommandCounterIncrement(), so
+		 * that the TOAST table will be visible for insertion.
+		 */
+		CommandCounterIncrement();
+
+		/* parse and validate reloptions for the toast table */
+		toast_options = transformRelOptions((Datum) 0,
+											create->options,
+											"toast",
+											validnsps,
+											true, false);
 
-	(void) heap_reloptions(RELKIND_TOASTVALUE, toast_options, true);
+		(void) heap_reloptions(RELKIND_TOASTVALUE, toast_options, true);
 
-	NewRelationCreateToastTable(intoRelationAddr.objectId, toast_options);
+		NewRelationCreateToastTable(intoRelationAddr.objectId, toast_options);
+	}
 
 	/* Create the "view" part of a materialized view. */
 	if (is_matview)
@@ -135,7 +231,7 @@ create_ctas_internal(List *attrList, IntoClause *into)
 		/* StoreViewQuery scribbles on tree, so make a copy */
 		Query	   *query = copyObject(into->viewQuery);
 
-		StoreViewQuery(intoRelationAddr.objectId, query, false);
+		StoreViewQuery(intoRelationAddr.objectId, query, replace);
 		CommandCounterIncrement();
 	}
 
@@ -232,7 +328,26 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt,
 
 	/* Check if the relation exists or not */
 	if (CreateTableAsRelExists(stmt))
+	{
+		/* An existing materialized view can be replaced. */
+		if (is_matview && into->replace)
+		{
+			RefreshMatViewStmt *refresh;
+
+			/* Change the relation to match the new query and other options. */
+			(void) create_ctas_nodata(query->targetList, into);
+
+			/* Refresh the materialized view with a fake statement. */
+			refresh = makeNode(RefreshMatViewStmt);
+			refresh->relation = into->rel;
+			refresh->skipData = into->skipData;
+			refresh->concurrent = false;
+
+			return ExecRefreshMatView(refresh, NULL, NULL);
+		}
+
 		return InvalidObjectAddress;
+	}
 
 	/*
 	 * Create the tuple receiver object and insert info it will need
@@ -400,14 +515,15 @@ CreateTableAsRelExists(CreateTableAsStmt *ctas)
 	oldrelid = get_relname_relid(into->rel->relname, nspid);
 	if (OidIsValid(oldrelid))
 	{
-		if (!ctas->if_not_exists)
+		if (!ctas->if_not_exists && !into->replace)
 			ereport(ERROR,
 					(errcode(ERRCODE_DUPLICATE_TABLE),
 					 errmsg("relation \"%s\" already exists",
 							into->rel->relname)));
 
 		/*
-		 * The relation exists and IF NOT EXISTS has been specified.
+		 * The relation exists and IF NOT EXISTS or OR REPLACE has been
+		 * specified.
 		 *
 		 * If we are in an extension script, insist that the pre-existing
 		 * object be a member of the extension, to avoid security risks.
@@ -415,11 +531,12 @@ CreateTableAsRelExists(CreateTableAsStmt *ctas)
 		ObjectAddressSet(address, RelationRelationId, oldrelid);
 		checkMembershipInCurrentExtension(&address);
 
-		/* OK to skip */
-		ereport(NOTICE,
-				(errcode(ERRCODE_DUPLICATE_TABLE),
-				 errmsg("relation \"%s\" already exists, skipping",
-						into->rel->relname)));
+		if (ctas->if_not_exists)
+			/* OK to skip */
+			ereport(NOTICE,
+					(errcode(ERRCODE_DUPLICATE_TABLE),
+					 errmsg("relation \"%s\" already exists, skipping",
+							into->rel->relname)));
 		return true;
 	}
 
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 4fc54bd6eb..12ba984a05 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -4589,7 +4589,7 @@ AlterTableGetLockLevel(List *cmds)
 				 * Subcommands that may be visible to concurrent SELECTs
 				 */
 			case AT_DropColumn: /* change visible to SELECT */
-			case AT_AddColumnToView:	/* CREATE VIEW */
+			case AT_AddColumnToView:	/* via CREATE OR REPLACE [MATERIALIZED] VIEW */
 			case AT_DropOids:	/* used to equiv to DropColumn */
 			case AT_EnableAlwaysRule:	/* may change SELECT rules */
 			case AT_EnableReplicaRule:	/* may change SELECT rules */
@@ -4884,8 +4884,8 @@ ATPrepCmd(List **wqueue, Relation rel, AlterTableCmd *cmd,
 			/* Recursion occurs during execution phase */
 			pass = AT_PASS_ADD_COL;
 			break;
-		case AT_AddColumnToView:	/* add column via CREATE OR REPLACE VIEW */
-			ATSimplePermissions(cmd->subtype, rel, ATT_VIEW);
+		case AT_AddColumnToView:	/* via CREATE OR REPLACE [MATERIALIZED] VIEW */
+			ATSimplePermissions(cmd->subtype, rel, ATT_VIEW | ATT_MATVIEW);
 			ATPrepAddColumn(wqueue, rel, recurse, recursing, true, cmd,
 							lockmode, context);
 			/* Recursion occurs during execution phase */
@@ -5314,7 +5314,7 @@ ATExecCmd(List **wqueue, AlteredTableInfo *tab,
 	switch (cmd->subtype)
 	{
 		case AT_AddColumn:		/* ADD COLUMN */
-		case AT_AddColumnToView:	/* add column via CREATE OR REPLACE VIEW */
+		case AT_AddColumnToView:	/* via CREATE OR REPLACE [MATERIALIZED] VIEW */
 			address = ATExecAddColumn(wqueue, tab, rel, &cmd,
 									  cmd->recurse, false,
 									  lockmode, cur_pass, context);
diff --git a/src/backend/commands/view.c b/src/backend/commands/view.c
index 6f0301555e..53d4cacc4c 100644
--- a/src/backend/commands/view.c
+++ b/src/backend/commands/view.c
@@ -30,8 +30,6 @@
 #include "utils/lsyscache.h"
 #include "utils/rel.h"
 
-static void checkViewColumns(TupleDesc newdesc, TupleDesc olddesc);
-
 /*---------------------------------------------------------------------
  * DefineVirtualRelation
  *
@@ -129,7 +127,7 @@ DefineVirtualRelation(RangeVar *relation, List *tlist, bool replace,
 		 * column list.
 		 */
 		descriptor = BuildDescForRelation(attrList);
-		checkViewColumns(descriptor, rel->rd_att);
+		checkViewColumns(descriptor, rel->rd_att, false);
 
 		/*
 		 * If new attributes have been added, we must add pg_attribute entries
@@ -263,15 +261,22 @@ DefineVirtualRelation(RangeVar *relation, List *tlist, bool replace,
  * added to generate specific complaints.  Also, we allow the new view to have
  * more columns than the old.
  */
-static void
-checkViewColumns(TupleDesc newdesc, TupleDesc olddesc)
+void
+checkViewColumns(TupleDesc newdesc, TupleDesc olddesc, bool is_matview)
 {
 	int			i;
 
 	if (newdesc->natts < olddesc->natts)
-		ereport(ERROR,
-				(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
-				 errmsg("cannot drop columns from view")));
+	{
+		if (is_matview)
+			ereport(ERROR,
+					errcode(ERRCODE_INVALID_TABLE_DEFINITION),
+					errmsg("cannot drop columns from materialized view"));
+		else
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
+					 errmsg("cannot drop columns from view")));
+	}
 
 	for (i = 0; i < olddesc->natts; i++)
 	{
@@ -280,17 +285,34 @@ checkViewColumns(TupleDesc newdesc, TupleDesc olddesc)
 
 		/* XXX msg not right, but we don't support DROP COL on view anyway */
 		if (newattr->attisdropped != oldattr->attisdropped)
-			ereport(ERROR,
-					(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
-					 errmsg("cannot drop columns from view")));
+		{
+			if (is_matview)
+				ereport(ERROR,
+						errcode(ERRCODE_INVALID_TABLE_DEFINITION),
+						errmsg("cannot drop columns from materialized view"));
+			else
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
+						 errmsg("cannot drop columns from view")));
+		}
 
 		if (strcmp(NameStr(newattr->attname), NameStr(oldattr->attname)) != 0)
-			ereport(ERROR,
-					(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
-					 errmsg("cannot change name of view column \"%s\" to \"%s\"",
-							NameStr(oldattr->attname),
-							NameStr(newattr->attname)),
-					 errhint("Use ALTER VIEW ... RENAME COLUMN ... to change name of view column instead.")));
+		{
+			if (is_matview)
+				ereport(ERROR,
+						errcode(ERRCODE_INVALID_TABLE_DEFINITION),
+						errmsg("cannot change name of materialized view column \"%s\" to \"%s\"",
+							   NameStr(oldattr->attname),
+							   NameStr(newattr->attname)),
+						errhint("Use ALTER MATERIALIZED VIEW ... RENAME COLUMN ... to change name of materialized view column instead."));
+			else
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
+						 errmsg("cannot change name of view column \"%s\" to \"%s\"",
+								NameStr(oldattr->attname),
+								NameStr(newattr->attname)),
+						 errhint("Use ALTER VIEW ... RENAME COLUMN ... to change name of view column instead.")));
+		}
 
 		/*
 		 * We cannot allow type, typmod, or collation to change, since these
@@ -299,26 +321,48 @@ checkViewColumns(TupleDesc newdesc, TupleDesc olddesc)
 		 */
 		if (newattr->atttypid != oldattr->atttypid ||
 			newattr->atttypmod != oldattr->atttypmod)
-			ereport(ERROR,
-					(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
-					 errmsg("cannot change data type of view column \"%s\" from %s to %s",
-							NameStr(oldattr->attname),
-							format_type_with_typemod(oldattr->atttypid,
-													 oldattr->atttypmod),
-							format_type_with_typemod(newattr->atttypid,
-													 newattr->atttypmod))));
+		{
+			if (is_matview)
+				ereport(ERROR,
+						errcode(ERRCODE_INVALID_TABLE_DEFINITION),
+						errmsg("cannot change data type of materialized view column \"%s\" from %s to %s",
+							   NameStr(oldattr->attname),
+							   format_type_with_typemod(oldattr->atttypid,
+														oldattr->atttypmod),
+							   format_type_with_typemod(newattr->atttypid,
+														newattr->atttypmod)));
+			else
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
+						 errmsg("cannot change data type of view column \"%s\" from %s to %s",
+								NameStr(oldattr->attname),
+								format_type_with_typemod(oldattr->atttypid,
+														 oldattr->atttypmod),
+								format_type_with_typemod(newattr->atttypid,
+														 newattr->atttypmod))));
+		}
 
 		/*
 		 * At this point, attcollations should be both valid or both invalid,
 		 * so applying get_collation_name unconditionally should be fine.
 		 */
 		if (newattr->attcollation != oldattr->attcollation)
-			ereport(ERROR,
-					(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
-					 errmsg("cannot change collation of view column \"%s\" from \"%s\" to \"%s\"",
-							NameStr(oldattr->attname),
-							get_collation_name(oldattr->attcollation),
-							get_collation_name(newattr->attcollation))));
+		{
+			if (is_matview)
+				ereport(ERROR,
+						errcode(ERRCODE_INVALID_TABLE_DEFINITION),
+						errmsg("cannot change collation of materialized view column \"%s\" from \"%s\" to \"%s\"",
+							   NameStr(oldattr->attname),
+							   get_collation_name(oldattr->attcollation),
+							   get_collation_name(newattr->attcollation)));
+			else
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
+						 errmsg("cannot change collation of view column \"%s\" from \"%s\" to \"%s\"",
+								NameStr(oldattr->attname),
+								get_collation_name(oldattr->attcollation),
+								get_collation_name(newattr->attcollation))));
+		}
 	}
 
 	/*
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 6079de70e0..c917c11f9e 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -4854,6 +4854,21 @@ CreateMatViewStmt:
 					$8->skipData = !($11);
 					$$ = (Node *) ctas;
 				}
+		| CREATE OR REPLACE OptNoLog MATERIALIZED VIEW create_mv_target AS SelectStmt opt_with_data
+				{
+					CreateTableAsStmt *ctas = makeNode(CreateTableAsStmt);
+
+					ctas->query = $9;
+					ctas->into = $7;
+					ctas->objtype = OBJECT_MATVIEW;
+					ctas->is_select_into = false;
+					ctas->if_not_exists = false;
+					/* cram additional flags into the IntoClause */
+					$7->rel->relpersistence = $4;
+					$7->skipData = !($10);
+					$7->replace = true;
+					$$ = (Node *) ctas;
+				}
 		;
 
 create_mv_target:
diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c
index 81cbf10aa2..dd72c617d5 100644
--- a/src/bin/psql/tab-complete.in.c
+++ b/src/bin/psql/tab-complete.in.c
@@ -2144,7 +2144,7 @@ match_previous_words(int pattern_id,
 	/* complete with something you can create or replace */
 	else if (TailMatches("CREATE", "OR", "REPLACE"))
 		COMPLETE_WITH("FUNCTION", "PROCEDURE", "LANGUAGE", "RULE", "VIEW",
-					  "AGGREGATE", "TRANSFORM", "TRIGGER");
+					  "AGGREGATE", "TRANSFORM", "TRIGGER", "MATERIALIZED VIEW");
 
 /* DROP, but not DROP embedded in other commands */
 	/* complete with something you can drop */
@@ -3979,28 +3979,34 @@ match_previous_words(int pattern_id,
 		COMPLETE_WITH("SELECT");
 
 /* CREATE MATERIALIZED VIEW */
-	else if (Matches("CREATE", "MATERIALIZED"))
+	else if (Matches("CREATE", "MATERIALIZED") ||
+			 Matches("CREATE", "OR", "REPLACE", "MATERIALIZED"))
 		COMPLETE_WITH("VIEW");
-	/* Complete CREATE MATERIALIZED VIEW <name> with AS or USING */
-	else if (Matches("CREATE", "MATERIALIZED", "VIEW", MatchAny))
+	/* Complete CREATE [ OR REPLACE ] MATERIALIZED VIEW <name> with AS or USING */
+	else if (Matches("CREATE", "MATERIALIZED", "VIEW", MatchAny) ||
+			 Matches("CREATE", "OR", "REPLACE", "MATERIALIZED", "VIEW", MatchAny))
 		COMPLETE_WITH("AS", "USING");
 
 	/*
-	 * Complete CREATE MATERIALIZED VIEW <name> USING with list of access
+	 * Complete CREATE [ OR REPLACE ] MATERIALIZED VIEW <name> USING with list of access
 	 * methods
 	 */
-	else if (Matches("CREATE", "MATERIALIZED", "VIEW", MatchAny, "USING"))
+	else if (Matches("CREATE", "MATERIALIZED", "VIEW", MatchAny, "USING") ||
+			 Matches("CREATE", "OR", "REPLACE", "MATERIALIZED", "VIEW", MatchAny, "USING"))
 		COMPLETE_WITH_QUERY(Query_for_list_of_table_access_methods);
-	/* Complete CREATE MATERIALIZED VIEW <name> USING <access method> with AS */
-	else if (Matches("CREATE", "MATERIALIZED", "VIEW", MatchAny, "USING", MatchAny))
+	/* Complete CREATE [ OR REPLACE ] MATERIALIZED VIEW <name> USING <access method> with AS */
+	else if (Matches("CREATE", "MATERIALIZED", "VIEW", MatchAny, "USING", MatchAny) ||
+			 Matches("CREATE", "OR", "REPLACE", "MATERIALIZED", "VIEW", MatchAny, "USING", MatchAny))
 		COMPLETE_WITH("AS");
 
 	/*
-	 * Complete CREATE MATERIALIZED VIEW <name> [USING <access method> ] AS
+	 * Complete CREATE [ OR REPLACE ] MATERIALIZED VIEW <name> [USING <access method> ] AS
 	 * with "SELECT"
 	 */
 	else if (Matches("CREATE", "MATERIALIZED", "VIEW", MatchAny, "AS") ||
-			 Matches("CREATE", "MATERIALIZED", "VIEW", MatchAny, "USING", MatchAny, "AS"))
+			 Matches("CREATE", "OR", "REPLACE", "MATERIALIZED", "VIEW", MatchAny, "AS") ||
+			 Matches("CREATE", "MATERIALIZED", "VIEW", MatchAny, "USING", MatchAny, "AS") ||
+			 Matches("CREATE", "OR", "REPLACE", "MATERIALIZED", "VIEW", MatchAny, "USING", MatchAny, "AS"))
 		COMPLETE_WITH("SELECT");
 
 /* CREATE EVENT TRIGGER */
diff --git a/src/include/commands/view.h b/src/include/commands/view.h
index c41f51b161..95290f0a1c 100644
--- a/src/include/commands/view.h
+++ b/src/include/commands/view.h
@@ -22,4 +22,7 @@ extern ObjectAddress DefineView(ViewStmt *stmt, const char *queryString,
 
 extern void StoreViewQuery(Oid viewOid, Query *viewParse, bool replace);
 
+extern void checkViewColumns(TupleDesc newdesc, TupleDesc olddesc,
+							 bool is_matview);
+
 #endif							/* VIEW_H */
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index b191eaaeca..f4c16e6a9e 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -2358,7 +2358,7 @@ typedef struct AlterTableStmt
 typedef enum AlterTableType
 {
 	AT_AddColumn,				/* add column */
-	AT_AddColumnToView,			/* implicitly via CREATE OR REPLACE VIEW */
+	AT_AddColumnToView,			/* implicitly via CREATE OR REPLACE [MATERIALIZED] VIEW */
 	AT_ColumnDefault,			/* alter column default */
 	AT_CookedColumnDefault,		/* add a pre-cooked column default */
 	AT_DropNotNull,				/* alter column drop not null */
diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h
index 9c2957eb54..fce057a8ac 100644
--- a/src/include/nodes/primnodes.h
+++ b/src/include/nodes/primnodes.h
@@ -168,6 +168,7 @@ typedef struct IntoClause
 	/* materialized view's SELECT query */
 	struct Query *viewQuery pg_node_attr(query_jumble_ignore);
 	bool		skipData;		/* true for WITH NO DATA */
+	bool		replace;		/* replace existing matview? */
 } IntoClause;
 
 
diff --git a/src/test/regress/expected/matview.out b/src/test/regress/expected/matview.out
index 9eab51bc2a..71f6a0681d 100644
--- a/src/test/regress/expected/matview.out
+++ b/src/test/regress/expected/matview.out
@@ -694,3 +694,194 @@ NOTICE:  relation "matview_ine_tab" already exists, skipping
 (0 rows)
 
 DROP MATERIALIZED VIEW matview_ine_tab;
+--
+-- test CREATE OR REPLACE MATERIALIZED VIEW
+--
+-- matview does not already exist
+DROP MATERIALIZED VIEW IF EXISTS mvtest_replace;
+NOTICE:  materialized view "mvtest_replace" does not exist, skipping
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 1 AS a;
+SELECT * FROM mvtest_replace;
+ a 
+---
+ 1
+(1 row)
+
+-- replace query with data
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 2 AS a;
+SELECT * FROM mvtest_replace;
+ a 
+---
+ 2
+(1 row)
+
+-- replace query without data
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 3 AS a
+  WITH NO DATA;
+SELECT * FROM mvtest_replace; -- error: not populated
+ERROR:  materialized view "mvtest_replace" has not been populated
+HINT:  Use the REFRESH MATERIALIZED VIEW command.
+REFRESH MATERIALIZED VIEW mvtest_replace;
+SELECT * FROM mvtest_replace;
+ a 
+---
+ 3
+(1 row)
+
+-- add column
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 4 AS a, 1 b;
+SELECT * FROM mvtest_replace;
+ a | b 
+---+---
+ 4 | 1
+(1 row)
+
+-- replace table options
+SELECT m.*, c.relname, c.reloptions, s.spcname, a.amname
+  FROM mvtest_replace m
+    CROSS JOIN pg_class c
+    LEFT JOIN pg_tablespace s ON s.oid = c.reltablespace
+    LEFT JOIN pg_am a ON a.oid = c.relam
+  WHERE c.relname = 'mvtest_replace';
+ a | b |    relname     | reloptions | spcname | amname 
+---+---+----------------+------------+---------+--------
+ 4 | 1 | mvtest_replace |            |         | heap
+(1 row)
+
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace
+  USING heap2
+  WITH (fillfactor = 50)
+  TABLESPACE regress_tblspace
+  AS SELECT 5 AS a, 1 AS b;
+SELECT m.*, c.relname, c.reloptions, s.spcname, a.amname
+  FROM mvtest_replace m
+    CROSS JOIN pg_class c
+    LEFT JOIN pg_tablespace s ON s.oid = c.reltablespace
+    LEFT JOIN pg_am a ON a.oid = c.relam
+  WHERE c.relname = 'mvtest_replace';
+ a | b |    relname     |   reloptions    |     spcname      | amname 
+---+---+----------------+-----------------+------------------+--------
+ 5 | 1 | mvtest_replace | {fillfactor=50} | regress_tblspace | heap2
+(1 row)
+
+-- can replace matview that has a dependent view
+CREATE VIEW mvtest_replace_v AS
+  SELECT * FROM mvtest_replace;
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 6 AS a, 1 AS b;
+SELECT * FROM mvtest_replace, mvtest_replace_v;
+ a | b | a | b 
+---+---+---+---
+ 6 | 1 | 6 | 1
+(1 row)
+
+DROP VIEW mvtest_replace_v;
+-- index gets rebuilt when replacing with data
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 7 AS a, 1 AS b;
+CREATE UNIQUE INDEX ON mvtest_replace (b);
+SELECT * FROM mvtest_replace;
+ a | b 
+---+---
+ 7 | 1
+(1 row)
+
+SET enable_seqscan = off; -- force index scan
+EXPLAIN (COSTS OFF) SELECT * FROM mvtest_replace WHERE b = 1;
+                       QUERY PLAN                        
+---------------------------------------------------------
+ Index Scan using mvtest_replace_b_idx on mvtest_replace
+   Index Cond: (b = 1)
+(2 rows)
+
+SELECT * FROM mvtest_replace WHERE b = 1;
+ a | b 
+---+---
+ 7 | 1
+(1 row)
+
+RESET enable_seqscan;
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 8 AS a, 1 AS b;
+SET enable_seqscan = off; -- force index scan
+EXPLAIN (COSTS OFF) SELECT * FROM mvtest_replace WHERE b = 1;
+                       QUERY PLAN                        
+---------------------------------------------------------
+ Index Scan using mvtest_replace_b_idx on mvtest_replace
+   Index Cond: (b = 1)
+(2 rows)
+
+SELECT * FROM mvtest_replace WHERE b = 1;
+ a | b 
+---+---
+ 8 | 1
+(1 row)
+
+RESET enable_seqscan;
+-- cannot change column data type
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 9 AS a, 'x' AS b; -- error
+ERROR:  cannot change data type of materialized view column "b" from integer to text
+SELECT * FROM mvtest_replace;
+ a | b 
+---+---
+ 8 | 1
+(1 row)
+
+-- cannot rename column
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 10 AS a, 1 AS b2; -- error
+ERROR:  cannot change name of materialized view column "b" to "b2"
+HINT:  Use ALTER MATERIALIZED VIEW ... RENAME COLUMN ... to change name of materialized view column instead.
+SELECT * FROM mvtest_replace;
+ a | b 
+---+---
+ 8 | 1
+(1 row)
+
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 11 AS a, 1 AS b, 'y' COLLATE "C" AS c;
+SELECT * FROM mvtest_replace;
+ a  | b | c 
+----+---+---
+ 11 | 1 | y
+(1 row)
+
+-- cannot change column collation
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 12 AS a, 1 AS b, 'x' COLLATE "POSIX" AS c; -- error
+ERROR:  cannot change collation of materialized view column "c" from "C" to "POSIX"
+SELECT * FROM mvtest_replace;
+ a  | b | c 
+----+---+---
+ 11 | 1 | y
+(1 row)
+
+-- cannot drop column
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 13 AS a, 1 AS b; -- error
+ERROR:  cannot drop columns from materialized view
+SELECT * FROM mvtest_replace;
+ a  | b | c 
+----+---+---
+ 11 | 1 | y
+(1 row)
+
+-- must target a matview
+CREATE VIEW mvtest_not_mv AS
+  SELECT 1 AS a;
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_not_mv AS
+  SELECT 1 AS a; -- error
+ERROR:  "mvtest_not_mv" is not a materialized view
+DROP VIEW mvtest_not_mv;
+-- cannot use OR REPLACE with IF NOT EXISTS
+CREATE OR REPLACE MATERIALIZED VIEW IF NOT EXISTS mvtest_replace AS
+  SELECT 1 AS a;
+ERROR:  syntax error at or near "NOT"
+LINE 1: CREATE OR REPLACE MATERIALIZED VIEW IF NOT EXISTS mvtest_rep...
+                                               ^
+DROP MATERIALIZED VIEW mvtest_replace;
diff --git a/src/test/regress/sql/matview.sql b/src/test/regress/sql/matview.sql
index 6704eeae2d..d6a4dc4b85 100644
--- a/src/test/regress/sql/matview.sql
+++ b/src/test/regress/sql/matview.sql
@@ -314,3 +314,111 @@ EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF)
   CREATE MATERIALIZED VIEW IF NOT EXISTS matview_ine_tab AS
     SELECT 1 / 0 WITH NO DATA; -- ok
 DROP MATERIALIZED VIEW matview_ine_tab;
+
+--
+-- test CREATE OR REPLACE MATERIALIZED VIEW
+--
+
+-- matview does not already exist
+DROP MATERIALIZED VIEW IF EXISTS mvtest_replace;
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 1 AS a;
+SELECT * FROM mvtest_replace;
+
+-- replace query with data
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 2 AS a;
+SELECT * FROM mvtest_replace;
+
+-- replace query without data
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 3 AS a
+  WITH NO DATA;
+SELECT * FROM mvtest_replace; -- error: not populated
+REFRESH MATERIALIZED VIEW mvtest_replace;
+SELECT * FROM mvtest_replace;
+
+-- add column
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 4 AS a, 1 b;
+SELECT * FROM mvtest_replace;
+
+-- replace table options
+SELECT m.*, c.relname, c.reloptions, s.spcname, a.amname
+  FROM mvtest_replace m
+    CROSS JOIN pg_class c
+    LEFT JOIN pg_tablespace s ON s.oid = c.reltablespace
+    LEFT JOIN pg_am a ON a.oid = c.relam
+  WHERE c.relname = 'mvtest_replace';
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace
+  USING heap2
+  WITH (fillfactor = 50)
+  TABLESPACE regress_tblspace
+  AS SELECT 5 AS a, 1 AS b;
+SELECT m.*, c.relname, c.reloptions, s.spcname, a.amname
+  FROM mvtest_replace m
+    CROSS JOIN pg_class c
+    LEFT JOIN pg_tablespace s ON s.oid = c.reltablespace
+    LEFT JOIN pg_am a ON a.oid = c.relam
+  WHERE c.relname = 'mvtest_replace';
+
+-- can replace matview that has a dependent view
+CREATE VIEW mvtest_replace_v AS
+  SELECT * FROM mvtest_replace;
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 6 AS a, 1 AS b;
+SELECT * FROM mvtest_replace, mvtest_replace_v;
+DROP VIEW mvtest_replace_v;
+
+-- index gets rebuilt when replacing with data
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 7 AS a, 1 AS b;
+CREATE UNIQUE INDEX ON mvtest_replace (b);
+SELECT * FROM mvtest_replace;
+SET enable_seqscan = off; -- force index scan
+EXPLAIN (COSTS OFF) SELECT * FROM mvtest_replace WHERE b = 1;
+SELECT * FROM mvtest_replace WHERE b = 1;
+RESET enable_seqscan;
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 8 AS a, 1 AS b;
+SET enable_seqscan = off; -- force index scan
+EXPLAIN (COSTS OFF) SELECT * FROM mvtest_replace WHERE b = 1;
+SELECT * FROM mvtest_replace WHERE b = 1;
+RESET enable_seqscan;
+
+-- cannot change column data type
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 9 AS a, 'x' AS b; -- error
+SELECT * FROM mvtest_replace;
+
+-- cannot rename column
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 10 AS a, 1 AS b2; -- error
+SELECT * FROM mvtest_replace;
+
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 11 AS a, 1 AS b, 'y' COLLATE "C" AS c;
+SELECT * FROM mvtest_replace;
+
+-- cannot change column collation
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 12 AS a, 1 AS b, 'x' COLLATE "POSIX" AS c; -- error
+SELECT * FROM mvtest_replace;
+
+-- cannot drop column
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 13 AS a, 1 AS b; -- error
+SELECT * FROM mvtest_replace;
+
+-- must target a matview
+CREATE VIEW mvtest_not_mv AS
+  SELECT 1 AS a;
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_not_mv AS
+  SELECT 1 AS a; -- error
+DROP VIEW mvtest_not_mv;
+
+-- cannot use OR REPLACE with IF NOT EXISTS
+CREATE OR REPLACE MATERIALIZED VIEW IF NOT EXISTS mvtest_replace AS
+  SELECT 1 AS a;
+
+DROP MATERIALIZED VIEW mvtest_replace;
-- 
2.48.0

  [text/plain] v5-0002-Deprecate-CREATE-MATERIALIZED-VIEW-IF-NOT-EXISTS.patch (11.0K, ../../64495e31-0a91-4894-87f8-31451ee69f44@ewie.name/3-v5-0002-Deprecate-CREATE-MATERIALIZED-VIEW-IF-NOT-EXISTS.patch)
  download | inline diff:
From 2478cd438360a2fd5d74082c96b8feabd49ac593 Mon Sep 17 00:00:00 2001
From: Erik Wienhold <ewie@ewie.name>
Date: Tue, 28 May 2024 02:19:53 +0200
Subject: [PATCH v5 2/3] Deprecate CREATE MATERIALIZED VIEW IF NOT EXISTS

---
 src/backend/parser/gram.y                     | 14 ++++++
 .../expected/test_extensions.out              | 45 +++++++++++++++++++
 src/test/regress/expected/matview.out         | 20 +++++++++
 3 files changed, 79 insertions(+)

diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index c917c11f9e..cb5647dcb6 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -4853,6 +4853,20 @@ CreateMatViewStmt:
 					$8->rel->relpersistence = $2;
 					$8->skipData = !($11);
 					$$ = (Node *) ctas;
+
+					if (ctas->into->rel->schemaname)
+							ereport(WARNING,
+									errmsg("IF NOT EXISTS is deprecated in materialized view creation"),
+									errhint("Use CREATE OR REPLACE MATERIALIZED VIEW %s.%s.",
+											ctas->into->rel->schemaname,
+											ctas->into->rel->relname),
+									parser_errposition(@1));
+					else
+							ereport(WARNING,
+									errmsg("IF NOT EXISTS is deprecated in materialized view creation"),
+									errhint("Use CREATE OR REPLACE MATERIALIZED VIEW %s.",
+											ctas->into->rel->relname),
+									parser_errposition(@1));
 				}
 		| CREATE OR REPLACE OptNoLog MATERIALIZED VIEW create_mv_target AS SelectStmt opt_with_data
 				{
diff --git a/src/test/modules/test_extensions/expected/test_extensions.out b/src/test/modules/test_extensions/expected/test_extensions.out
index d5388a1fec..9a1e0f7658 100644
--- a/src/test/modules/test_extensions/expected/test_extensions.out
+++ b/src/test/modules/test_extensions/expected/test_extensions.out
@@ -404,6 +404,11 @@ Objects in extension "test_ext_cor"
 CREATE COLLATION ext_cine_coll
   ( LC_COLLATE = "C", LC_CTYPE = "C" );
 CREATE EXTENSION test_ext_cine;  -- fail
+WARNING:  IF NOT EXISTS is deprecated in materialized view creation
+LINE 1: CREATE MATERIALIZED VIEW IF NOT EXISTS ext_cine_mv AS SELECT...
+        ^
+HINT:  Use CREATE OR REPLACE MATERIALIZED VIEW ext_cine_mv.
+QUERY:  CREATE MATERIALIZED VIEW IF NOT EXISTS ext_cine_mv AS SELECT 42 AS f1;
 ERROR:  collation ext_cine_coll is not a member of extension "test_ext_cine"
 DETAIL:  An extension may only use CREATE ... IF NOT EXISTS to skip object creation if the conflicting object is one that it already owns.
 CONTEXT:  SQL statement "CREATE COLLATION IF NOT EXISTS ext_cine_coll
@@ -412,6 +417,11 @@ extension script file "test_ext_cine--1.0.sql", near line 10
 DROP COLLATION ext_cine_coll;
 CREATE MATERIALIZED VIEW ext_cine_mv AS SELECT 11 AS f1;
 CREATE EXTENSION test_ext_cine;  -- fail
+WARNING:  IF NOT EXISTS is deprecated in materialized view creation
+LINE 1: CREATE MATERIALIZED VIEW IF NOT EXISTS ext_cine_mv AS SELECT...
+        ^
+HINT:  Use CREATE OR REPLACE MATERIALIZED VIEW ext_cine_mv.
+QUERY:  CREATE MATERIALIZED VIEW IF NOT EXISTS ext_cine_mv AS SELECT 42 AS f1;
 ERROR:  materialized view ext_cine_mv is not a member of extension "test_ext_cine"
 DETAIL:  An extension may only use CREATE ... IF NOT EXISTS to skip object creation if the conflicting object is one that it already owns.
 CONTEXT:  SQL statement "CREATE MATERIALIZED VIEW IF NOT EXISTS ext_cine_mv AS SELECT 42 AS f1"
@@ -420,6 +430,11 @@ DROP MATERIALIZED VIEW ext_cine_mv;
 CREATE FOREIGN DATA WRAPPER dummy;
 CREATE SERVER ext_cine_srv FOREIGN DATA WRAPPER dummy;
 CREATE EXTENSION test_ext_cine;  -- fail
+WARNING:  IF NOT EXISTS is deprecated in materialized view creation
+LINE 1: CREATE MATERIALIZED VIEW IF NOT EXISTS ext_cine_mv AS SELECT...
+        ^
+HINT:  Use CREATE OR REPLACE MATERIALIZED VIEW ext_cine_mv.
+QUERY:  CREATE MATERIALIZED VIEW IF NOT EXISTS ext_cine_mv AS SELECT 42 AS f1;
 ERROR:  server ext_cine_srv is not a member of extension "test_ext_cine"
 DETAIL:  An extension may only use CREATE ... IF NOT EXISTS to skip object creation if the conflicting object is one that it already owns.
 CONTEXT:  SQL statement "CREATE SERVER IF NOT EXISTS ext_cine_srv FOREIGN DATA WRAPPER ext_cine_fdw"
@@ -427,6 +442,11 @@ extension script file "test_ext_cine--1.0.sql", near line 17
 DROP SERVER ext_cine_srv;
 CREATE SCHEMA ext_cine_schema;
 CREATE EXTENSION test_ext_cine;  -- fail
+WARNING:  IF NOT EXISTS is deprecated in materialized view creation
+LINE 1: CREATE MATERIALIZED VIEW IF NOT EXISTS ext_cine_mv AS SELECT...
+        ^
+HINT:  Use CREATE OR REPLACE MATERIALIZED VIEW ext_cine_mv.
+QUERY:  CREATE MATERIALIZED VIEW IF NOT EXISTS ext_cine_mv AS SELECT 42 AS f1;
 ERROR:  schema ext_cine_schema is not a member of extension "test_ext_cine"
 DETAIL:  An extension may only use CREATE ... IF NOT EXISTS to skip object creation if the conflicting object is one that it already owns.
 CONTEXT:  SQL statement "CREATE SCHEMA IF NOT EXISTS ext_cine_schema"
@@ -434,6 +454,11 @@ extension script file "test_ext_cine--1.0.sql", near line 19
 DROP SCHEMA ext_cine_schema;
 CREATE SEQUENCE ext_cine_seq;
 CREATE EXTENSION test_ext_cine;  -- fail
+WARNING:  IF NOT EXISTS is deprecated in materialized view creation
+LINE 1: CREATE MATERIALIZED VIEW IF NOT EXISTS ext_cine_mv AS SELECT...
+        ^
+HINT:  Use CREATE OR REPLACE MATERIALIZED VIEW ext_cine_mv.
+QUERY:  CREATE MATERIALIZED VIEW IF NOT EXISTS ext_cine_mv AS SELECT 42 AS f1;
 ERROR:  sequence ext_cine_seq is not a member of extension "test_ext_cine"
 DETAIL:  An extension may only use CREATE ... IF NOT EXISTS to skip object creation if the conflicting object is one that it already owns.
 CONTEXT:  SQL statement "CREATE SEQUENCE IF NOT EXISTS ext_cine_seq"
@@ -441,6 +466,11 @@ extension script file "test_ext_cine--1.0.sql", near line 21
 DROP SEQUENCE ext_cine_seq;
 CREATE TABLE ext_cine_tab1 (x int);
 CREATE EXTENSION test_ext_cine;  -- fail
+WARNING:  IF NOT EXISTS is deprecated in materialized view creation
+LINE 1: CREATE MATERIALIZED VIEW IF NOT EXISTS ext_cine_mv AS SELECT...
+        ^
+HINT:  Use CREATE OR REPLACE MATERIALIZED VIEW ext_cine_mv.
+QUERY:  CREATE MATERIALIZED VIEW IF NOT EXISTS ext_cine_mv AS SELECT 42 AS f1;
 ERROR:  table ext_cine_tab1 is not a member of extension "test_ext_cine"
 DETAIL:  An extension may only use CREATE ... IF NOT EXISTS to skip object creation if the conflicting object is one that it already owns.
 CONTEXT:  SQL statement "CREATE TABLE IF NOT EXISTS ext_cine_tab1 (x int)"
@@ -448,12 +478,22 @@ extension script file "test_ext_cine--1.0.sql", near line 23
 DROP TABLE ext_cine_tab1;
 CREATE TABLE ext_cine_tab2 AS SELECT 42 AS y;
 CREATE EXTENSION test_ext_cine;  -- fail
+WARNING:  IF NOT EXISTS is deprecated in materialized view creation
+LINE 1: CREATE MATERIALIZED VIEW IF NOT EXISTS ext_cine_mv AS SELECT...
+        ^
+HINT:  Use CREATE OR REPLACE MATERIALIZED VIEW ext_cine_mv.
+QUERY:  CREATE MATERIALIZED VIEW IF NOT EXISTS ext_cine_mv AS SELECT 42 AS f1;
 ERROR:  table ext_cine_tab2 is not a member of extension "test_ext_cine"
 DETAIL:  An extension may only use CREATE ... IF NOT EXISTS to skip object creation if the conflicting object is one that it already owns.
 CONTEXT:  SQL statement "CREATE TABLE IF NOT EXISTS ext_cine_tab2 AS SELECT 42 AS y"
 extension script file "test_ext_cine--1.0.sql", near line 25
 DROP TABLE ext_cine_tab2;
 CREATE EXTENSION test_ext_cine;
+WARNING:  IF NOT EXISTS is deprecated in materialized view creation
+LINE 1: CREATE MATERIALIZED VIEW IF NOT EXISTS ext_cine_mv AS SELECT...
+        ^
+HINT:  Use CREATE OR REPLACE MATERIALIZED VIEW ext_cine_mv.
+QUERY:  CREATE MATERIALIZED VIEW IF NOT EXISTS ext_cine_mv AS SELECT 42 AS f1;
 \dx+ test_ext_cine
 Objects in extension "test_ext_cine"
         Object description         
@@ -475,6 +515,11 @@ Objects in extension "test_ext_cine"
 (14 rows)
 
 ALTER EXTENSION test_ext_cine UPDATE TO '1.1';
+WARNING:  IF NOT EXISTS is deprecated in materialized view creation
+LINE 1: CREATE MATERIALIZED VIEW IF NOT EXISTS ext_cine_mv AS SELECT...
+        ^
+HINT:  Use CREATE OR REPLACE MATERIALIZED VIEW ext_cine_mv.
+QUERY:  CREATE MATERIALIZED VIEW IF NOT EXISTS ext_cine_mv AS SELECT 42 AS f1;
 \dx+ test_ext_cine
 Objects in extension "test_ext_cine"
         Object description         
diff --git a/src/test/regress/expected/matview.out b/src/test/regress/expected/matview.out
index 71f6a0681d..d506c615da 100644
--- a/src/test/regress/expected/matview.out
+++ b/src/test/regress/expected/matview.out
@@ -565,6 +565,10 @@ CREATE MATERIALIZED VIEW mvtest_mv_foo AS SELECT * FROM mvtest_foo_data;
 CREATE MATERIALIZED VIEW mvtest_mv_foo AS SELECT * FROM mvtest_foo_data;
 ERROR:  relation "mvtest_mv_foo" already exists
 CREATE MATERIALIZED VIEW IF NOT EXISTS mvtest_mv_foo AS SELECT * FROM mvtest_foo_data;
+WARNING:  IF NOT EXISTS is deprecated in materialized view creation
+LINE 1: CREATE MATERIALIZED VIEW IF NOT EXISTS mvtest_mv_foo AS SELE...
+        ^
+HINT:  Use CREATE OR REPLACE MATERIALIZED VIEW mvtest_mv_foo.
 NOTICE:  relation "mvtest_mv_foo" already exists, skipping
 CREATE UNIQUE INDEX ON mvtest_mv_foo (i);
 RESET ROLE;
@@ -662,12 +666,20 @@ CREATE MATERIALIZED VIEW matview_ine_tab AS SELECT 1 / 0; -- error
 ERROR:  relation "matview_ine_tab" already exists
 CREATE MATERIALIZED VIEW IF NOT EXISTS matview_ine_tab AS
   SELECT 1 / 0; -- ok
+WARNING:  IF NOT EXISTS is deprecated in materialized view creation
+LINE 1: CREATE MATERIALIZED VIEW IF NOT EXISTS matview_ine_tab AS
+        ^
+HINT:  Use CREATE OR REPLACE MATERIALIZED VIEW matview_ine_tab.
 NOTICE:  relation "matview_ine_tab" already exists, skipping
 CREATE MATERIALIZED VIEW matview_ine_tab AS
   SELECT 1 / 0 WITH NO DATA; -- error
 ERROR:  relation "matview_ine_tab" already exists
 CREATE MATERIALIZED VIEW IF NOT EXISTS matview_ine_tab AS
   SELECT 1 / 0 WITH NO DATA; -- ok
+WARNING:  IF NOT EXISTS is deprecated in materialized view creation
+LINE 1: CREATE MATERIALIZED VIEW IF NOT EXISTS matview_ine_tab AS
+        ^
+HINT:  Use CREATE OR REPLACE MATERIALIZED VIEW matview_ine_tab.
 NOTICE:  relation "matview_ine_tab" already exists, skipping
 EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF)
   CREATE MATERIALIZED VIEW matview_ine_tab AS
@@ -676,6 +688,10 @@ ERROR:  relation "matview_ine_tab" already exists
 EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF)
   CREATE MATERIALIZED VIEW IF NOT EXISTS matview_ine_tab AS
     SELECT 1 / 0; -- ok
+WARNING:  IF NOT EXISTS is deprecated in materialized view creation
+LINE 2:   CREATE MATERIALIZED VIEW IF NOT EXISTS matview_ine_tab AS
+          ^
+HINT:  Use CREATE OR REPLACE MATERIALIZED VIEW matview_ine_tab.
 NOTICE:  relation "matview_ine_tab" already exists, skipping
  QUERY PLAN 
 ------------
@@ -688,6 +704,10 @@ ERROR:  relation "matview_ine_tab" already exists
 EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF)
   CREATE MATERIALIZED VIEW IF NOT EXISTS matview_ine_tab AS
     SELECT 1 / 0 WITH NO DATA; -- ok
+WARNING:  IF NOT EXISTS is deprecated in materialized view creation
+LINE 2:   CREATE MATERIALIZED VIEW IF NOT EXISTS matview_ine_tab AS
+          ^
+HINT:  Use CREATE OR REPLACE MATERIALIZED VIEW matview_ine_tab.
 NOTICE:  relation "matview_ine_tab" already exists, skipping
  QUERY PLAN 
 ------------
-- 
2.48.0

  [text/plain] v5-0003-Replace-matview-WITH-OLD-DATA.patch (7.7K, ../../64495e31-0a91-4894-87f8-31451ee69f44@ewie.name/4-v5-0003-Replace-matview-WITH-OLD-DATA.patch)
  download | inline diff:
From 68c8209174f48479df34f37b9ad303a737571473 Mon Sep 17 00:00:00 2001
From: Erik Wienhold <ewie@ewie.name>
Date: Fri, 26 Jul 2024 23:33:15 +0200
Subject: [PATCH v5 3/3] Replace matview WITH OLD DATA

---
 .../sgml/ref/create_materialized_view.sgml    | 16 +++++++++--
 src/backend/commands/createas.c               | 26 +++++++++++------
 src/backend/parser/gram.y                     | 16 +++++++++++
 src/include/nodes/primnodes.h                 |  1 +
 src/test/regress/expected/matview.out         | 28 +++++++++++++++++++
 src/test/regress/sql/matview.sql              | 15 ++++++++++
 6 files changed, 90 insertions(+), 12 deletions(-)

diff --git a/doc/src/sgml/ref/create_materialized_view.sgml b/doc/src/sgml/ref/create_materialized_view.sgml
index 5e03320eb7..1352e9de40 100644
--- a/doc/src/sgml/ref/create_materialized_view.sgml
+++ b/doc/src/sgml/ref/create_materialized_view.sgml
@@ -27,7 +27,7 @@ CREATE [ OR REPLACE ] MATERIALIZED VIEW [ IF NOT EXISTS ] <replaceable>table_nam
     [ WITH ( <replaceable class="parameter">storage_parameter</replaceable> [= <replaceable class="parameter">value</replaceable>] [, ... ] ) ]
     [ TABLESPACE <replaceable class="parameter">tablespace_name</replaceable> ]
     AS <replaceable>query</replaceable>
-    [ WITH [ NO ] DATA ]
+    [ WITH [ NO | OLD ] DATA ]
 </synopsis>
  </refsynopsisdiv>
 
@@ -37,7 +37,8 @@ CREATE [ OR REPLACE ] MATERIALIZED VIEW [ IF NOT EXISTS ] <replaceable>table_nam
   <para>
    <command>CREATE MATERIALIZED VIEW</command> defines a materialized view of
    a query.  The query is executed and used to populate the view at the time
-   the command is issued (unless <command>WITH NO DATA</command> is used) and may be
+   the command is issued (unless <command>WITH NO DATA</command> or
+   <command>WITH OLD DATA</command> is used) and may be
    refreshed later using <command>REFRESH MATERIALIZED VIEW</command>.
   </para>
 
@@ -162,7 +163,7 @@ CREATE [ OR REPLACE ] MATERIALIZED VIEW [ IF NOT EXISTS ] <replaceable>table_nam
    </varlistentry>
 
    <varlistentry>
-    <term><literal>WITH [ NO ] DATA</literal></term>
+    <term><literal>WITH [ NO | OLD ] DATA</literal></term>
     <listitem>
      <para>
       This clause specifies whether or not the materialized view should be
@@ -170,6 +171,15 @@ CREATE [ OR REPLACE ] MATERIALIZED VIEW [ IF NOT EXISTS ] <replaceable>table_nam
       flagged as unscannable and cannot be queried until <command>REFRESH
       MATERIALIZED VIEW</command> is used.
      </para>
+
+     <para>
+      The form <command>WITH OLD DATA</command> keeps the already stored data
+      when replacing an existing materialized view to keep it populated.  For
+      newly created materialized views, this has the same effect as
+      <command>WITH DATA</command>.  Use this form if you want to use
+      <command>REFRESH MATERIALIZED VIEW CONCURRENTLY</command> as it requires
+      a populated materialized view.
+     </para>
     </listitem>
    </varlistentry>
 
diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c
index cba369114b..1af714fe4a 100644
--- a/src/backend/commands/createas.c
+++ b/src/backend/commands/createas.c
@@ -332,18 +332,26 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt,
 		/* An existing materialized view can be replaced. */
 		if (is_matview && into->replace)
 		{
-			RefreshMatViewStmt *refresh;
-
 			/* Change the relation to match the new query and other options. */
-			(void) create_ctas_nodata(query->targetList, into);
+			address = create_ctas_nodata(query->targetList, into);
 
-			/* Refresh the materialized view with a fake statement. */
-			refresh = makeNode(RefreshMatViewStmt);
-			refresh->relation = into->rel;
-			refresh->skipData = into->skipData;
-			refresh->concurrent = false;
+			/*
+			 * Refresh the materialized view with a fake statement unless we
+			 * must keep the old data.
+			 */
+			if (!into->keepData)
+			{
+				RefreshMatViewStmt *refresh;
+
+				refresh = makeNode(RefreshMatViewStmt);
+				refresh->relation = into->rel;
+				refresh->skipData = into->skipData;
+				refresh->concurrent = false;
+
+				address = ExecRefreshMatView(refresh, NULL, NULL);
+			}
 
-			return ExecRefreshMatView(refresh, NULL, NULL);
+			return address;
 		}
 
 		return InvalidObjectAddress;
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index cb5647dcb6..9e7041f98e 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -4883,6 +4883,22 @@ CreateMatViewStmt:
 					$7->replace = true;
 					$$ = (Node *) ctas;
 				}
+		| CREATE OR REPLACE OptNoLog MATERIALIZED VIEW create_mv_target AS SelectStmt WITH OLD DATA_P
+				{
+					CreateTableAsStmt *ctas = makeNode(CreateTableAsStmt);
+
+					ctas->query = $9;
+					ctas->into = $7;
+					ctas->objtype = OBJECT_MATVIEW;
+					ctas->is_select_into = false;
+					ctas->if_not_exists = false;
+					/* cram additional flags into the IntoClause */
+					$7->rel->relpersistence = $4;
+					$7->skipData = false;
+					$7->keepData = true;
+					$7->replace = true;
+					$$ = (Node *) ctas;
+				}
 		;
 
 create_mv_target:
diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h
index fce057a8ac..793c971133 100644
--- a/src/include/nodes/primnodes.h
+++ b/src/include/nodes/primnodes.h
@@ -168,6 +168,7 @@ typedef struct IntoClause
 	/* materialized view's SELECT query */
 	struct Query *viewQuery pg_node_attr(query_jumble_ignore);
 	bool		skipData;		/* true for WITH NO DATA */
+	bool		keepData;		/* true for WITH OLD DATA */
 	bool		replace;		/* replace existing matview? */
 } IntoClause;
 
diff --git a/src/test/regress/expected/matview.out b/src/test/regress/expected/matview.out
index d506c615da..04c2095c74 100644
--- a/src/test/regress/expected/matview.out
+++ b/src/test/regress/expected/matview.out
@@ -751,6 +751,23 @@ SELECT * FROM mvtest_replace;
  3
 (1 row)
 
+-- replace query but keep old data
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 5 AS a
+  WITH OLD DATA;
+SELECT * FROM mvtest_replace;
+ a 
+---
+ 3
+(1 row)
+
+REFRESH MATERIALIZED VIEW mvtest_replace;
+SELECT * FROM mvtest_replace;
+ a 
+---
+ 5
+(1 row)
+
 -- add column
 CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
   SELECT 4 AS a, 1 b;
@@ -905,3 +922,14 @@ ERROR:  syntax error at or near "NOT"
 LINE 1: CREATE OR REPLACE MATERIALIZED VIEW IF NOT EXISTS mvtest_rep...
                                                ^
 DROP MATERIALIZED VIEW mvtest_replace;
+-- Create new matview WITH OLD DATA.  This populates the new matview as if
+-- WITH DATA had been specified.
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 17 AS a
+  WITH OLD DATA;
+SELECT * FROM mvtest_replace;
+ a  
+----
+ 17
+(1 row)
+
diff --git a/src/test/regress/sql/matview.sql b/src/test/regress/sql/matview.sql
index d6a4dc4b85..91f547e9cb 100644
--- a/src/test/regress/sql/matview.sql
+++ b/src/test/regress/sql/matview.sql
@@ -338,6 +338,14 @@ SELECT * FROM mvtest_replace; -- error: not populated
 REFRESH MATERIALIZED VIEW mvtest_replace;
 SELECT * FROM mvtest_replace;
 
+-- replace query but keep old data
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 5 AS a
+  WITH OLD DATA;
+SELECT * FROM mvtest_replace;
+REFRESH MATERIALIZED VIEW mvtest_replace;
+SELECT * FROM mvtest_replace;
+
 -- add column
 CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
   SELECT 4 AS a, 1 b;
@@ -422,3 +430,10 @@ CREATE OR REPLACE MATERIALIZED VIEW IF NOT EXISTS mvtest_replace AS
   SELECT 1 AS a;
 
 DROP MATERIALIZED VIEW mvtest_replace;
+
+-- Create new matview WITH OLD DATA.  This populates the new matview as if
+-- WITH DATA had been specified.
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 17 AS a
+  WITH OLD DATA;
+SELECT * FROM mvtest_replace;
-- 
2.48.0

  [application/pgp-signature] signature.asc (227B, ../../64495e31-0a91-4894-87f8-31451ee69f44@ewie.name/5-signature.asc)
  download

^ permalink  raw  reply  [nested|flat] 29+ messages in thread

* Re: CREATE OR REPLACE MATERIALIZED VIEW
@ 2025-03-10 02:46  Erik Wienhold <ewie@ewie.name>
  parent: Erik Wienhold <ewie@ewie.name>
  0 siblings, 1 reply; 29+ messages in thread

From: Erik Wienhold @ 2025-03-10 02:46 UTC (permalink / raw)
  To: Said Assemlal <sassemlal@neurorx.com>; +Cc: pgsql-hackers

The attached v6 fixes the build.  Somehow I missed testing with
--with-cassert the whole time and it turned that out I forgot to pass
queryString to ExecRefreshMatView.

-- 
Erik Wienhold
From 6ec126d8da5ca80f93ea8a58e07d654f5e21ef6d Mon Sep 17 00:00:00 2001
From: Erik Wienhold <ewie@ewie.name>
Date: Tue, 21 May 2024 18:35:47 +0200
Subject: [PATCH v6 1/3] Add CREATE OR REPLACE MATERIALIZED VIEW

---
 .../sgml/ref/create_materialized_view.sgml    |  15 +-
 src/backend/commands/createas.c               | 207 ++++++++++++++----
 src/backend/commands/tablecmds.c              |   8 +-
 src/backend/commands/view.c                   | 106 ++++++---
 src/backend/parser/gram.y                     |  15 ++
 src/bin/psql/tab-complete.in.c                |  26 ++-
 src/include/commands/view.h                   |   3 +
 src/include/nodes/parsenodes.h                |   2 +-
 src/include/nodes/primnodes.h                 |   1 +
 src/test/regress/expected/matview.out         | 191 ++++++++++++++++
 src/test/regress/sql/matview.sql              | 108 +++++++++
 11 files changed, 589 insertions(+), 93 deletions(-)

diff --git a/doc/src/sgml/ref/create_materialized_view.sgml b/doc/src/sgml/ref/create_materialized_view.sgml
index 62d897931c3..5e03320eb73 100644
--- a/doc/src/sgml/ref/create_materialized_view.sgml
+++ b/doc/src/sgml/ref/create_materialized_view.sgml
@@ -21,7 +21,7 @@ PostgreSQL documentation
 
  <refsynopsisdiv>
 <synopsis>
-CREATE MATERIALIZED VIEW [ IF NOT EXISTS ] <replaceable>table_name</replaceable>
+CREATE [ OR REPLACE ] MATERIALIZED VIEW [ IF NOT EXISTS ] <replaceable>table_name</replaceable>
     [ (<replaceable>column_name</replaceable> [, ...] ) ]
     [ USING <replaceable class="parameter">method</replaceable> ]
     [ WITH ( <replaceable class="parameter">storage_parameter</replaceable> [= <replaceable class="parameter">value</replaceable>] [, ... ] ) ]
@@ -60,6 +60,17 @@ CREATE MATERIALIZED VIEW [ IF NOT EXISTS ] <replaceable>table_name</replaceable>
   <title>Parameters</title>
 
   <variablelist>
+   <varlistentry>
+    <term><literal>OR REPLACE</literal></term>
+    <listitem>
+     <para>
+      Replaces a materialized view if it already exists.
+      Specifying <literal>OR REPLACE</literal> together with
+      <literal>IF NOT EXISTS</literal> is an error.
+     </para>
+    </listitem>
+   </varlistentry>
+
    <varlistentry>
     <term><literal>IF NOT EXISTS</literal></term>
     <listitem>
@@ -67,7 +78,7 @@ CREATE MATERIALIZED VIEW [ IF NOT EXISTS ] <replaceable>table_name</replaceable>
       Do not throw an error if a materialized view with the same name already
       exists. A notice is issued in this case.  Note that there is no guarantee
       that the existing materialized view is anything like the one that would
-      have been created.
+      have been created, unless you use <literal>OR REPLACE</literal> instead.
      </para>
     </listitem>
    </varlistentry>
diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c
index 44b4665ccd3..9f37ed7f177 100644
--- a/src/backend/commands/createas.c
+++ b/src/backend/commands/createas.c
@@ -79,55 +79,151 @@ static void intorel_destroy(DestReceiver *self);
 static ObjectAddress
 create_ctas_internal(List *attrList, IntoClause *into)
 {
-	CreateStmt *create = makeNode(CreateStmt);
-	bool		is_matview;
+	bool		is_matview,
+				replace = false;
 	char		relkind;
-	Datum		toast_options;
-	const char *const validnsps[] = HEAP_RELOPT_NAMESPACES;
+	Oid			matviewOid = InvalidOid;
 	ObjectAddress intoRelationAddr;
 
 	/* This code supports both CREATE TABLE AS and CREATE MATERIALIZED VIEW */
 	is_matview = (into->viewQuery != NULL);
 	relkind = is_matview ? RELKIND_MATVIEW : RELKIND_RELATION;
 
-	/*
-	 * Create the target relation by faking up a CREATE TABLE parsetree and
-	 * passing it to DefineRelation.
-	 */
-	create->relation = into->rel;
-	create->tableElts = attrList;
-	create->inhRelations = NIL;
-	create->ofTypename = NULL;
-	create->constraints = NIL;
-	create->options = into->options;
-	create->oncommit = into->onCommit;
-	create->tablespacename = into->tableSpaceName;
-	create->if_not_exists = false;
-	create->accessMethod = into->accessMethod;
+	/* Check if an existing materialized view needs to be replaced. */
+	if (is_matview)
+	{
+		LOCKMODE	lockmode;
 
-	/*
-	 * Create the relation.  (This will error out if there's an existing view,
-	 * so we don't need more code to complain if "replace" is false.)
-	 */
-	intoRelationAddr = DefineRelation(create, relkind, InvalidOid, NULL, NULL);
+		lockmode = into->replace ? AccessExclusiveLock : NoLock;
+		(void) RangeVarGetAndCheckCreationNamespace(into->rel, lockmode,
+													&matviewOid);
+		replace = OidIsValid(matviewOid) && into->replace;
+	}
 
-	/*
-	 * If necessary, create a TOAST table for the target table.  Note that
-	 * NewRelationCreateToastTable ends with CommandCounterIncrement(), so
-	 * that the TOAST table will be visible for insertion.
-	 */
-	CommandCounterIncrement();
+	if (is_matview && replace)
+	{
+		Relation	rel;
+		List	   *atcmds = NIL;
+		AlterTableCmd *atcmd;
+		TupleDesc	descriptor;
+
+		rel = relation_open(matviewOid, NoLock);
+
+		if (rel->rd_rel->relkind != RELKIND_MATVIEW)
+			ereport(ERROR,
+					errcode(ERRCODE_WRONG_OBJECT_TYPE),
+					errmsg("\"%s\" is not a materialized view",
+						   RelationGetRelationName(rel)));
+
+		CheckTableNotInUse(rel, "CREATE OR REPLACE MATERIALIZED VIEW");
+
+		descriptor = BuildDescForRelation(attrList);
+		checkViewColumns(descriptor, rel->rd_att, true);
+
+		/* Add new attributes via ALTER TABLE. */
+		if (list_length(attrList) > rel->rd_att->natts)
+		{
+			ListCell   *c;
+			int			skip = rel->rd_att->natts;
+
+			foreach(c, attrList)
+			{
+				if (skip > 0)
+				{
+					skip--;
+					continue;
+				}
+				atcmd = makeNode(AlterTableCmd);
+				atcmd->subtype = AT_AddColumnToView;
+				atcmd->def = (Node *) lfirst(c);
+				atcmds = lappend(atcmds, atcmd);
+			}
+		}
+
+		/* Set access method via ALTER TABLE. */
+		if (into->accessMethod != NULL)
+		{
+			atcmd = makeNode(AlterTableCmd);
+			atcmd->subtype = AT_SetAccessMethod;
+			atcmd->name = into->accessMethod;
+			atcmds = lappend(atcmds, atcmd);
+		}
+
+		/* Set tablespace via ALTER TABLE. */
+		if (into->tableSpaceName != NULL)
+		{
+			atcmd = makeNode(AlterTableCmd);
+			atcmd->subtype = AT_SetTableSpace;
+			atcmd->name = into->tableSpaceName;
+			atcmds = lappend(atcmds, atcmd);
+		}
+
+		/* Set storage parameters via ALTER TABLE. */
+		if (into->options != NIL)
+		{
+			atcmd = makeNode(AlterTableCmd);
+			atcmd->subtype = AT_ReplaceRelOptions;
+			atcmd->def = (Node *) into->options;
+			atcmds = lappend(atcmds, atcmd);
+		}
+
+		if (atcmds != NIL)
+		{
+			AlterTableInternal(matviewOid, atcmds, true);
+			CommandCounterIncrement();
+		}
+
+		relation_close(rel, NoLock);
+
+		ObjectAddressSet(intoRelationAddr, RelationRelationId, matviewOid);
+	}
+	else
+	{
+		CreateStmt *create = makeNode(CreateStmt);
+		Datum		toast_options;
+		const static char *validnsps[] = HEAP_RELOPT_NAMESPACES;
+
+		/*
+		 * Create the target relation by faking up a CREATE TABLE parsetree
+		 * and passing it to DefineRelation.
+		 */
+		create->relation = into->rel;
+		create->tableElts = attrList;
+		create->inhRelations = NIL;
+		create->ofTypename = NULL;
+		create->constraints = NIL;
+		create->options = into->options;
+		create->oncommit = into->onCommit;
+		create->tablespacename = into->tableSpaceName;
+		create->if_not_exists = false;
+		create->accessMethod = into->accessMethod;
+
+		/*
+		 * Create the relation.  (This will error out if there's an existing
+		 * view, so we don't need more code to complain if "replace" is
+		 * false.)
+		 */
+		intoRelationAddr = DefineRelation(create, relkind, InvalidOid, NULL,
+										  NULL);
 
-	/* parse and validate reloptions for the toast table */
-	toast_options = transformRelOptions((Datum) 0,
-										create->options,
-										"toast",
-										validnsps,
-										true, false);
+		/*
+		 * If necessary, create a TOAST table for the target table.  Note that
+		 * NewRelationCreateToastTable ends with CommandCounterIncrement(), so
+		 * that the TOAST table will be visible for insertion.
+		 */
+		CommandCounterIncrement();
+
+		/* parse and validate reloptions for the toast table */
+		toast_options = transformRelOptions((Datum) 0,
+											create->options,
+											"toast",
+											validnsps,
+											true, false);
 
-	(void) heap_reloptions(RELKIND_TOASTVALUE, toast_options, true);
+		(void) heap_reloptions(RELKIND_TOASTVALUE, toast_options, true);
 
-	NewRelationCreateToastTable(intoRelationAddr.objectId, toast_options);
+		NewRelationCreateToastTable(intoRelationAddr.objectId, toast_options);
+	}
 
 	/* Create the "view" part of a materialized view. */
 	if (is_matview)
@@ -135,7 +231,7 @@ create_ctas_internal(List *attrList, IntoClause *into)
 		/* StoreViewQuery scribbles on tree, so make a copy */
 		Query	   *query = copyObject(into->viewQuery);
 
-		StoreViewQuery(intoRelationAddr.objectId, query, false);
+		StoreViewQuery(intoRelationAddr.objectId, query, replace);
 		CommandCounterIncrement();
 	}
 
@@ -232,7 +328,26 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt,
 
 	/* Check if the relation exists or not */
 	if (CreateTableAsRelExists(stmt))
+	{
+		/* An existing materialized view can be replaced. */
+		if (is_matview && into->replace)
+		{
+			RefreshMatViewStmt *refresh;
+
+			/* Change the relation to match the new query and other options. */
+			(void) create_ctas_nodata(query->targetList, into);
+
+			/* Refresh the materialized view with a fake statement. */
+			refresh = makeNode(RefreshMatViewStmt);
+			refresh->relation = into->rel;
+			refresh->skipData = into->skipData;
+			refresh->concurrent = false;
+
+			return ExecRefreshMatView(refresh, pstate->p_sourcetext, qc);
+		}
+
 		return InvalidObjectAddress;
+	}
 
 	/*
 	 * Create the tuple receiver object and insert info it will need
@@ -401,14 +516,15 @@ CreateTableAsRelExists(CreateTableAsStmt *ctas)
 	oldrelid = get_relname_relid(into->rel->relname, nspid);
 	if (OidIsValid(oldrelid))
 	{
-		if (!ctas->if_not_exists)
+		if (!ctas->if_not_exists && !into->replace)
 			ereport(ERROR,
 					(errcode(ERRCODE_DUPLICATE_TABLE),
 					 errmsg("relation \"%s\" already exists",
 							into->rel->relname)));
 
 		/*
-		 * The relation exists and IF NOT EXISTS has been specified.
+		 * The relation exists and IF NOT EXISTS or OR REPLACE has been
+		 * specified.
 		 *
 		 * If we are in an extension script, insist that the pre-existing
 		 * object be a member of the extension, to avoid security risks.
@@ -416,11 +532,12 @@ CreateTableAsRelExists(CreateTableAsStmt *ctas)
 		ObjectAddressSet(address, RelationRelationId, oldrelid);
 		checkMembershipInCurrentExtension(&address);
 
-		/* OK to skip */
-		ereport(NOTICE,
-				(errcode(ERRCODE_DUPLICATE_TABLE),
-				 errmsg("relation \"%s\" already exists, skipping",
-						into->rel->relname)));
+		if (ctas->if_not_exists)
+			/* OK to skip */
+			ereport(NOTICE,
+					(errcode(ERRCODE_DUPLICATE_TABLE),
+					 errmsg("relation \"%s\" already exists, skipping",
+							into->rel->relname)));
 		return true;
 	}
 
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 59156a1c1f6..49713bc0422 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -4605,7 +4605,7 @@ AlterTableGetLockLevel(List *cmds)
 				 * Subcommands that may be visible to concurrent SELECTs
 				 */
 			case AT_DropColumn: /* change visible to SELECT */
-			case AT_AddColumnToView:	/* CREATE VIEW */
+			case AT_AddColumnToView:	/* via CREATE OR REPLACE [MATERIALIZED] VIEW */
 			case AT_DropOids:	/* used to equiv to DropColumn */
 			case AT_EnableAlwaysRule:	/* may change SELECT rules */
 			case AT_EnableReplicaRule:	/* may change SELECT rules */
@@ -4900,8 +4900,8 @@ ATPrepCmd(List **wqueue, Relation rel, AlterTableCmd *cmd,
 			/* Recursion occurs during execution phase */
 			pass = AT_PASS_ADD_COL;
 			break;
-		case AT_AddColumnToView:	/* add column via CREATE OR REPLACE VIEW */
-			ATSimplePermissions(cmd->subtype, rel, ATT_VIEW);
+		case AT_AddColumnToView:	/* via CREATE OR REPLACE [MATERIALIZED] VIEW */
+			ATSimplePermissions(cmd->subtype, rel, ATT_VIEW | ATT_MATVIEW);
 			ATPrepAddColumn(wqueue, rel, recurse, recursing, true, cmd,
 							lockmode, context);
 			/* Recursion occurs during execution phase */
@@ -5332,7 +5332,7 @@ ATExecCmd(List **wqueue, AlteredTableInfo *tab,
 	switch (cmd->subtype)
 	{
 		case AT_AddColumn:		/* ADD COLUMN */
-		case AT_AddColumnToView:	/* add column via CREATE OR REPLACE VIEW */
+		case AT_AddColumnToView:	/* via CREATE OR REPLACE [MATERIALIZED] VIEW */
 			address = ATExecAddColumn(wqueue, tab, rel, &cmd,
 									  cmd->recurse, false,
 									  lockmode, cur_pass, context);
diff --git a/src/backend/commands/view.c b/src/backend/commands/view.c
index 6f0301555e0..53d4cacc4c1 100644
--- a/src/backend/commands/view.c
+++ b/src/backend/commands/view.c
@@ -30,8 +30,6 @@
 #include "utils/lsyscache.h"
 #include "utils/rel.h"
 
-static void checkViewColumns(TupleDesc newdesc, TupleDesc olddesc);
-
 /*---------------------------------------------------------------------
  * DefineVirtualRelation
  *
@@ -129,7 +127,7 @@ DefineVirtualRelation(RangeVar *relation, List *tlist, bool replace,
 		 * column list.
 		 */
 		descriptor = BuildDescForRelation(attrList);
-		checkViewColumns(descriptor, rel->rd_att);
+		checkViewColumns(descriptor, rel->rd_att, false);
 
 		/*
 		 * If new attributes have been added, we must add pg_attribute entries
@@ -263,15 +261,22 @@ DefineVirtualRelation(RangeVar *relation, List *tlist, bool replace,
  * added to generate specific complaints.  Also, we allow the new view to have
  * more columns than the old.
  */
-static void
-checkViewColumns(TupleDesc newdesc, TupleDesc olddesc)
+void
+checkViewColumns(TupleDesc newdesc, TupleDesc olddesc, bool is_matview)
 {
 	int			i;
 
 	if (newdesc->natts < olddesc->natts)
-		ereport(ERROR,
-				(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
-				 errmsg("cannot drop columns from view")));
+	{
+		if (is_matview)
+			ereport(ERROR,
+					errcode(ERRCODE_INVALID_TABLE_DEFINITION),
+					errmsg("cannot drop columns from materialized view"));
+		else
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
+					 errmsg("cannot drop columns from view")));
+	}
 
 	for (i = 0; i < olddesc->natts; i++)
 	{
@@ -280,17 +285,34 @@ checkViewColumns(TupleDesc newdesc, TupleDesc olddesc)
 
 		/* XXX msg not right, but we don't support DROP COL on view anyway */
 		if (newattr->attisdropped != oldattr->attisdropped)
-			ereport(ERROR,
-					(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
-					 errmsg("cannot drop columns from view")));
+		{
+			if (is_matview)
+				ereport(ERROR,
+						errcode(ERRCODE_INVALID_TABLE_DEFINITION),
+						errmsg("cannot drop columns from materialized view"));
+			else
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
+						 errmsg("cannot drop columns from view")));
+		}
 
 		if (strcmp(NameStr(newattr->attname), NameStr(oldattr->attname)) != 0)
-			ereport(ERROR,
-					(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
-					 errmsg("cannot change name of view column \"%s\" to \"%s\"",
-							NameStr(oldattr->attname),
-							NameStr(newattr->attname)),
-					 errhint("Use ALTER VIEW ... RENAME COLUMN ... to change name of view column instead.")));
+		{
+			if (is_matview)
+				ereport(ERROR,
+						errcode(ERRCODE_INVALID_TABLE_DEFINITION),
+						errmsg("cannot change name of materialized view column \"%s\" to \"%s\"",
+							   NameStr(oldattr->attname),
+							   NameStr(newattr->attname)),
+						errhint("Use ALTER MATERIALIZED VIEW ... RENAME COLUMN ... to change name of materialized view column instead."));
+			else
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
+						 errmsg("cannot change name of view column \"%s\" to \"%s\"",
+								NameStr(oldattr->attname),
+								NameStr(newattr->attname)),
+						 errhint("Use ALTER VIEW ... RENAME COLUMN ... to change name of view column instead.")));
+		}
 
 		/*
 		 * We cannot allow type, typmod, or collation to change, since these
@@ -299,26 +321,48 @@ checkViewColumns(TupleDesc newdesc, TupleDesc olddesc)
 		 */
 		if (newattr->atttypid != oldattr->atttypid ||
 			newattr->atttypmod != oldattr->atttypmod)
-			ereport(ERROR,
-					(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
-					 errmsg("cannot change data type of view column \"%s\" from %s to %s",
-							NameStr(oldattr->attname),
-							format_type_with_typemod(oldattr->atttypid,
-													 oldattr->atttypmod),
-							format_type_with_typemod(newattr->atttypid,
-													 newattr->atttypmod))));
+		{
+			if (is_matview)
+				ereport(ERROR,
+						errcode(ERRCODE_INVALID_TABLE_DEFINITION),
+						errmsg("cannot change data type of materialized view column \"%s\" from %s to %s",
+							   NameStr(oldattr->attname),
+							   format_type_with_typemod(oldattr->atttypid,
+														oldattr->atttypmod),
+							   format_type_with_typemod(newattr->atttypid,
+														newattr->atttypmod)));
+			else
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
+						 errmsg("cannot change data type of view column \"%s\" from %s to %s",
+								NameStr(oldattr->attname),
+								format_type_with_typemod(oldattr->atttypid,
+														 oldattr->atttypmod),
+								format_type_with_typemod(newattr->atttypid,
+														 newattr->atttypmod))));
+		}
 
 		/*
 		 * At this point, attcollations should be both valid or both invalid,
 		 * so applying get_collation_name unconditionally should be fine.
 		 */
 		if (newattr->attcollation != oldattr->attcollation)
-			ereport(ERROR,
-					(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
-					 errmsg("cannot change collation of view column \"%s\" from \"%s\" to \"%s\"",
-							NameStr(oldattr->attname),
-							get_collation_name(oldattr->attcollation),
-							get_collation_name(newattr->attcollation))));
+		{
+			if (is_matview)
+				ereport(ERROR,
+						errcode(ERRCODE_INVALID_TABLE_DEFINITION),
+						errmsg("cannot change collation of materialized view column \"%s\" from \"%s\" to \"%s\"",
+							   NameStr(oldattr->attname),
+							   get_collation_name(oldattr->attcollation),
+							   get_collation_name(newattr->attcollation)));
+			else
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
+						 errmsg("cannot change collation of view column \"%s\" from \"%s\" to \"%s\"",
+								NameStr(oldattr->attname),
+								get_collation_name(oldattr->attcollation),
+								get_collation_name(newattr->attcollation))));
+		}
 	}
 
 	/*
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 271ae26cbaf..d2edc460378 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -4895,6 +4895,21 @@ CreateMatViewStmt:
 					$8->skipData = !($11);
 					$$ = (Node *) ctas;
 				}
+		| CREATE OR REPLACE OptNoLog MATERIALIZED VIEW create_mv_target AS SelectStmt opt_with_data
+				{
+					CreateTableAsStmt *ctas = makeNode(CreateTableAsStmt);
+
+					ctas->query = $9;
+					ctas->into = $7;
+					ctas->objtype = OBJECT_MATVIEW;
+					ctas->is_select_into = false;
+					ctas->if_not_exists = false;
+					/* cram additional flags into the IntoClause */
+					$7->rel->relpersistence = $4;
+					$7->skipData = !($10);
+					$7->replace = true;
+					$$ = (Node *) ctas;
+				}
 		;
 
 create_mv_target:
diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c
index 8432be641ac..e862b135a73 100644
--- a/src/bin/psql/tab-complete.in.c
+++ b/src/bin/psql/tab-complete.in.c
@@ -2162,7 +2162,7 @@ match_previous_words(int pattern_id,
 	/* complete with something you can create or replace */
 	else if (TailMatches("CREATE", "OR", "REPLACE"))
 		COMPLETE_WITH("FUNCTION", "PROCEDURE", "LANGUAGE", "RULE", "VIEW",
-					  "AGGREGATE", "TRANSFORM", "TRIGGER");
+					  "AGGREGATE", "TRANSFORM", "TRIGGER", "MATERIALIZED VIEW");
 
 /* DROP, but not DROP embedded in other commands */
 	/* complete with something you can drop */
@@ -4008,28 +4008,34 @@ match_previous_words(int pattern_id,
 		COMPLETE_WITH("SELECT");
 
 /* CREATE MATERIALIZED VIEW */
-	else if (Matches("CREATE", "MATERIALIZED"))
+	else if (Matches("CREATE", "MATERIALIZED") ||
+			 Matches("CREATE", "OR", "REPLACE", "MATERIALIZED"))
 		COMPLETE_WITH("VIEW");
-	/* Complete CREATE MATERIALIZED VIEW <name> with AS or USING */
-	else if (Matches("CREATE", "MATERIALIZED", "VIEW", MatchAny))
+	/* Complete CREATE [ OR REPLACE ] MATERIALIZED VIEW <name> with AS or USING */
+	else if (Matches("CREATE", "MATERIALIZED", "VIEW", MatchAny) ||
+			 Matches("CREATE", "OR", "REPLACE", "MATERIALIZED", "VIEW", MatchAny))
 		COMPLETE_WITH("AS", "USING");
 
 	/*
-	 * Complete CREATE MATERIALIZED VIEW <name> USING with list of access
+	 * Complete CREATE [ OR REPLACE ] MATERIALIZED VIEW <name> USING with list of access
 	 * methods
 	 */
-	else if (Matches("CREATE", "MATERIALIZED", "VIEW", MatchAny, "USING"))
+	else if (Matches("CREATE", "MATERIALIZED", "VIEW", MatchAny, "USING") ||
+			 Matches("CREATE", "OR", "REPLACE", "MATERIALIZED", "VIEW", MatchAny, "USING"))
 		COMPLETE_WITH_QUERY(Query_for_list_of_table_access_methods);
-	/* Complete CREATE MATERIALIZED VIEW <name> USING <access method> with AS */
-	else if (Matches("CREATE", "MATERIALIZED", "VIEW", MatchAny, "USING", MatchAny))
+	/* Complete CREATE [ OR REPLACE ] MATERIALIZED VIEW <name> USING <access method> with AS */
+	else if (Matches("CREATE", "MATERIALIZED", "VIEW", MatchAny, "USING", MatchAny) ||
+			 Matches("CREATE", "OR", "REPLACE", "MATERIALIZED", "VIEW", MatchAny, "USING", MatchAny))
 		COMPLETE_WITH("AS");
 
 	/*
-	 * Complete CREATE MATERIALIZED VIEW <name> [USING <access method> ] AS
+	 * Complete CREATE [ OR REPLACE ] MATERIALIZED VIEW <name> [USING <access method> ] AS
 	 * with "SELECT"
 	 */
 	else if (Matches("CREATE", "MATERIALIZED", "VIEW", MatchAny, "AS") ||
-			 Matches("CREATE", "MATERIALIZED", "VIEW", MatchAny, "USING", MatchAny, "AS"))
+			 Matches("CREATE", "OR", "REPLACE", "MATERIALIZED", "VIEW", MatchAny, "AS") ||
+			 Matches("CREATE", "MATERIALIZED", "VIEW", MatchAny, "USING", MatchAny, "AS") ||
+			 Matches("CREATE", "OR", "REPLACE", "MATERIALIZED", "VIEW", MatchAny, "USING", MatchAny, "AS"))
 		COMPLETE_WITH("SELECT");
 
 /* CREATE EVENT TRIGGER */
diff --git a/src/include/commands/view.h b/src/include/commands/view.h
index c41f51b161c..95290f0a1c4 100644
--- a/src/include/commands/view.h
+++ b/src/include/commands/view.h
@@ -22,4 +22,7 @@ extern ObjectAddress DefineView(ViewStmt *stmt, const char *queryString,
 
 extern void StoreViewQuery(Oid viewOid, Query *viewParse, bool replace);
 
+extern void checkViewColumns(TupleDesc newdesc, TupleDesc olddesc,
+							 bool is_matview);
+
 #endif							/* VIEW_H */
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 23c9e3c5abf..5a25ba96ab3 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -2402,7 +2402,7 @@ typedef struct AlterTableStmt
 typedef enum AlterTableType
 {
 	AT_AddColumn,				/* add column */
-	AT_AddColumnToView,			/* implicitly via CREATE OR REPLACE VIEW */
+	AT_AddColumnToView,			/* implicitly via CREATE OR REPLACE [MATERIALIZED] VIEW */
 	AT_ColumnDefault,			/* alter column default */
 	AT_CookedColumnDefault,		/* add a pre-cooked column default */
 	AT_DropNotNull,				/* alter column drop not null */
diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h
index d0576da3e25..e800d13a6e0 100644
--- a/src/include/nodes/primnodes.h
+++ b/src/include/nodes/primnodes.h
@@ -169,6 +169,7 @@ typedef struct IntoClause
 	/* materialized view's SELECT query */
 	struct Query *viewQuery pg_node_attr(query_jumble_ignore);
 	bool		skipData;		/* true for WITH NO DATA */
+	bool		replace;		/* replace existing matview? */
 } IntoClause;
 
 
diff --git a/src/test/regress/expected/matview.out b/src/test/regress/expected/matview.out
index 54939ecc6b0..c0cf8ecbffc 100644
--- a/src/test/regress/expected/matview.out
+++ b/src/test/regress/expected/matview.out
@@ -694,3 +694,194 @@ NOTICE:  relation "matview_ine_tab" already exists, skipping
 (0 rows)
 
 DROP MATERIALIZED VIEW matview_ine_tab;
+--
+-- test CREATE OR REPLACE MATERIALIZED VIEW
+--
+-- matview does not already exist
+DROP MATERIALIZED VIEW IF EXISTS mvtest_replace;
+NOTICE:  materialized view "mvtest_replace" does not exist, skipping
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 1 AS a;
+SELECT * FROM mvtest_replace;
+ a 
+---
+ 1
+(1 row)
+
+-- replace query with data
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 2 AS a;
+SELECT * FROM mvtest_replace;
+ a 
+---
+ 2
+(1 row)
+
+-- replace query without data
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 3 AS a
+  WITH NO DATA;
+SELECT * FROM mvtest_replace; -- error: not populated
+ERROR:  materialized view "mvtest_replace" has not been populated
+HINT:  Use the REFRESH MATERIALIZED VIEW command.
+REFRESH MATERIALIZED VIEW mvtest_replace;
+SELECT * FROM mvtest_replace;
+ a 
+---
+ 3
+(1 row)
+
+-- add column
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 4 AS a, 1 b;
+SELECT * FROM mvtest_replace;
+ a | b 
+---+---
+ 4 | 1
+(1 row)
+
+-- replace table options
+SELECT m.*, c.relname, c.reloptions, s.spcname, a.amname
+  FROM mvtest_replace m
+    CROSS JOIN pg_class c
+    LEFT JOIN pg_tablespace s ON s.oid = c.reltablespace
+    LEFT JOIN pg_am a ON a.oid = c.relam
+  WHERE c.relname = 'mvtest_replace';
+ a | b |    relname     | reloptions | spcname | amname 
+---+---+----------------+------------+---------+--------
+ 4 | 1 | mvtest_replace |            |         | heap
+(1 row)
+
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace
+  USING heap2
+  WITH (fillfactor = 50)
+  TABLESPACE regress_tblspace
+  AS SELECT 5 AS a, 1 AS b;
+SELECT m.*, c.relname, c.reloptions, s.spcname, a.amname
+  FROM mvtest_replace m
+    CROSS JOIN pg_class c
+    LEFT JOIN pg_tablespace s ON s.oid = c.reltablespace
+    LEFT JOIN pg_am a ON a.oid = c.relam
+  WHERE c.relname = 'mvtest_replace';
+ a | b |    relname     |   reloptions    |     spcname      | amname 
+---+---+----------------+-----------------+------------------+--------
+ 5 | 1 | mvtest_replace | {fillfactor=50} | regress_tblspace | heap2
+(1 row)
+
+-- can replace matview that has a dependent view
+CREATE VIEW mvtest_replace_v AS
+  SELECT * FROM mvtest_replace;
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 6 AS a, 1 AS b;
+SELECT * FROM mvtest_replace, mvtest_replace_v;
+ a | b | a | b 
+---+---+---+---
+ 6 | 1 | 6 | 1
+(1 row)
+
+DROP VIEW mvtest_replace_v;
+-- index gets rebuilt when replacing with data
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 7 AS a, 1 AS b;
+CREATE UNIQUE INDEX ON mvtest_replace (b);
+SELECT * FROM mvtest_replace;
+ a | b 
+---+---
+ 7 | 1
+(1 row)
+
+SET enable_seqscan = off; -- force index scan
+EXPLAIN (COSTS OFF) SELECT * FROM mvtest_replace WHERE b = 1;
+                       QUERY PLAN                        
+---------------------------------------------------------
+ Index Scan using mvtest_replace_b_idx on mvtest_replace
+   Index Cond: (b = 1)
+(2 rows)
+
+SELECT * FROM mvtest_replace WHERE b = 1;
+ a | b 
+---+---
+ 7 | 1
+(1 row)
+
+RESET enable_seqscan;
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 8 AS a, 1 AS b;
+SET enable_seqscan = off; -- force index scan
+EXPLAIN (COSTS OFF) SELECT * FROM mvtest_replace WHERE b = 1;
+                       QUERY PLAN                        
+---------------------------------------------------------
+ Index Scan using mvtest_replace_b_idx on mvtest_replace
+   Index Cond: (b = 1)
+(2 rows)
+
+SELECT * FROM mvtest_replace WHERE b = 1;
+ a | b 
+---+---
+ 8 | 1
+(1 row)
+
+RESET enable_seqscan;
+-- cannot change column data type
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 9 AS a, 'x' AS b; -- error
+ERROR:  cannot change data type of materialized view column "b" from integer to text
+SELECT * FROM mvtest_replace;
+ a | b 
+---+---
+ 8 | 1
+(1 row)
+
+-- cannot rename column
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 10 AS a, 1 AS b2; -- error
+ERROR:  cannot change name of materialized view column "b" to "b2"
+HINT:  Use ALTER MATERIALIZED VIEW ... RENAME COLUMN ... to change name of materialized view column instead.
+SELECT * FROM mvtest_replace;
+ a | b 
+---+---
+ 8 | 1
+(1 row)
+
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 11 AS a, 1 AS b, 'y' COLLATE "C" AS c;
+SELECT * FROM mvtest_replace;
+ a  | b | c 
+----+---+---
+ 11 | 1 | y
+(1 row)
+
+-- cannot change column collation
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 12 AS a, 1 AS b, 'x' COLLATE "POSIX" AS c; -- error
+ERROR:  cannot change collation of materialized view column "c" from "C" to "POSIX"
+SELECT * FROM mvtest_replace;
+ a  | b | c 
+----+---+---
+ 11 | 1 | y
+(1 row)
+
+-- cannot drop column
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 13 AS a, 1 AS b; -- error
+ERROR:  cannot drop columns from materialized view
+SELECT * FROM mvtest_replace;
+ a  | b | c 
+----+---+---
+ 11 | 1 | y
+(1 row)
+
+-- must target a matview
+CREATE VIEW mvtest_not_mv AS
+  SELECT 1 AS a;
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_not_mv AS
+  SELECT 1 AS a; -- error
+ERROR:  "mvtest_not_mv" is not a materialized view
+DROP VIEW mvtest_not_mv;
+-- cannot use OR REPLACE with IF NOT EXISTS
+CREATE OR REPLACE MATERIALIZED VIEW IF NOT EXISTS mvtest_replace AS
+  SELECT 1 AS a;
+ERROR:  syntax error at or near "NOT"
+LINE 1: CREATE OR REPLACE MATERIALIZED VIEW IF NOT EXISTS mvtest_rep...
+                                               ^
+DROP MATERIALIZED VIEW mvtest_replace;
diff --git a/src/test/regress/sql/matview.sql b/src/test/regress/sql/matview.sql
index 6704eeae2df..d6a4dc4b857 100644
--- a/src/test/regress/sql/matview.sql
+++ b/src/test/regress/sql/matview.sql
@@ -314,3 +314,111 @@ EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF)
   CREATE MATERIALIZED VIEW IF NOT EXISTS matview_ine_tab AS
     SELECT 1 / 0 WITH NO DATA; -- ok
 DROP MATERIALIZED VIEW matview_ine_tab;
+
+--
+-- test CREATE OR REPLACE MATERIALIZED VIEW
+--
+
+-- matview does not already exist
+DROP MATERIALIZED VIEW IF EXISTS mvtest_replace;
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 1 AS a;
+SELECT * FROM mvtest_replace;
+
+-- replace query with data
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 2 AS a;
+SELECT * FROM mvtest_replace;
+
+-- replace query without data
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 3 AS a
+  WITH NO DATA;
+SELECT * FROM mvtest_replace; -- error: not populated
+REFRESH MATERIALIZED VIEW mvtest_replace;
+SELECT * FROM mvtest_replace;
+
+-- add column
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 4 AS a, 1 b;
+SELECT * FROM mvtest_replace;
+
+-- replace table options
+SELECT m.*, c.relname, c.reloptions, s.spcname, a.amname
+  FROM mvtest_replace m
+    CROSS JOIN pg_class c
+    LEFT JOIN pg_tablespace s ON s.oid = c.reltablespace
+    LEFT JOIN pg_am a ON a.oid = c.relam
+  WHERE c.relname = 'mvtest_replace';
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace
+  USING heap2
+  WITH (fillfactor = 50)
+  TABLESPACE regress_tblspace
+  AS SELECT 5 AS a, 1 AS b;
+SELECT m.*, c.relname, c.reloptions, s.spcname, a.amname
+  FROM mvtest_replace m
+    CROSS JOIN pg_class c
+    LEFT JOIN pg_tablespace s ON s.oid = c.reltablespace
+    LEFT JOIN pg_am a ON a.oid = c.relam
+  WHERE c.relname = 'mvtest_replace';
+
+-- can replace matview that has a dependent view
+CREATE VIEW mvtest_replace_v AS
+  SELECT * FROM mvtest_replace;
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 6 AS a, 1 AS b;
+SELECT * FROM mvtest_replace, mvtest_replace_v;
+DROP VIEW mvtest_replace_v;
+
+-- index gets rebuilt when replacing with data
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 7 AS a, 1 AS b;
+CREATE UNIQUE INDEX ON mvtest_replace (b);
+SELECT * FROM mvtest_replace;
+SET enable_seqscan = off; -- force index scan
+EXPLAIN (COSTS OFF) SELECT * FROM mvtest_replace WHERE b = 1;
+SELECT * FROM mvtest_replace WHERE b = 1;
+RESET enable_seqscan;
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 8 AS a, 1 AS b;
+SET enable_seqscan = off; -- force index scan
+EXPLAIN (COSTS OFF) SELECT * FROM mvtest_replace WHERE b = 1;
+SELECT * FROM mvtest_replace WHERE b = 1;
+RESET enable_seqscan;
+
+-- cannot change column data type
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 9 AS a, 'x' AS b; -- error
+SELECT * FROM mvtest_replace;
+
+-- cannot rename column
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 10 AS a, 1 AS b2; -- error
+SELECT * FROM mvtest_replace;
+
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 11 AS a, 1 AS b, 'y' COLLATE "C" AS c;
+SELECT * FROM mvtest_replace;
+
+-- cannot change column collation
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 12 AS a, 1 AS b, 'x' COLLATE "POSIX" AS c; -- error
+SELECT * FROM mvtest_replace;
+
+-- cannot drop column
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 13 AS a, 1 AS b; -- error
+SELECT * FROM mvtest_replace;
+
+-- must target a matview
+CREATE VIEW mvtest_not_mv AS
+  SELECT 1 AS a;
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_not_mv AS
+  SELECT 1 AS a; -- error
+DROP VIEW mvtest_not_mv;
+
+-- cannot use OR REPLACE with IF NOT EXISTS
+CREATE OR REPLACE MATERIALIZED VIEW IF NOT EXISTS mvtest_replace AS
+  SELECT 1 AS a;
+
+DROP MATERIALIZED VIEW mvtest_replace;
-- 
2.48.1
From 232b1439f6cb5d931540901b8e582bd4e6551abf Mon Sep 17 00:00:00 2001
From: Erik Wienhold <ewie@ewie.name>
Date: Tue, 28 May 2024 02:19:53 +0200
Subject: [PATCH v6 2/3] Deprecate CREATE MATERIALIZED VIEW IF NOT EXISTS

---
 src/backend/parser/gram.y                     | 14 ++++++
 .../expected/test_extensions.out              | 45 +++++++++++++++++++
 src/test/regress/expected/matview.out         | 20 +++++++++
 3 files changed, 79 insertions(+)

diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index d2edc460378..8be790eaccf 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -4894,6 +4894,20 @@ CreateMatViewStmt:
 					$8->rel->relpersistence = $2;
 					$8->skipData = !($11);
 					$$ = (Node *) ctas;
+
+					if (ctas->into->rel->schemaname)
+							ereport(WARNING,
+									errmsg("IF NOT EXISTS is deprecated in materialized view creation"),
+									errhint("Use CREATE OR REPLACE MATERIALIZED VIEW %s.%s.",
+											ctas->into->rel->schemaname,
+											ctas->into->rel->relname),
+									parser_errposition(@1));
+					else
+							ereport(WARNING,
+									errmsg("IF NOT EXISTS is deprecated in materialized view creation"),
+									errhint("Use CREATE OR REPLACE MATERIALIZED VIEW %s.",
+											ctas->into->rel->relname),
+									parser_errposition(@1));
 				}
 		| CREATE OR REPLACE OptNoLog MATERIALIZED VIEW create_mv_target AS SelectStmt opt_with_data
 				{
diff --git a/src/test/modules/test_extensions/expected/test_extensions.out b/src/test/modules/test_extensions/expected/test_extensions.out
index d5388a1fecf..9a1e0f76587 100644
--- a/src/test/modules/test_extensions/expected/test_extensions.out
+++ b/src/test/modules/test_extensions/expected/test_extensions.out
@@ -404,6 +404,11 @@ Objects in extension "test_ext_cor"
 CREATE COLLATION ext_cine_coll
   ( LC_COLLATE = "C", LC_CTYPE = "C" );
 CREATE EXTENSION test_ext_cine;  -- fail
+WARNING:  IF NOT EXISTS is deprecated in materialized view creation
+LINE 1: CREATE MATERIALIZED VIEW IF NOT EXISTS ext_cine_mv AS SELECT...
+        ^
+HINT:  Use CREATE OR REPLACE MATERIALIZED VIEW ext_cine_mv.
+QUERY:  CREATE MATERIALIZED VIEW IF NOT EXISTS ext_cine_mv AS SELECT 42 AS f1;
 ERROR:  collation ext_cine_coll is not a member of extension "test_ext_cine"
 DETAIL:  An extension may only use CREATE ... IF NOT EXISTS to skip object creation if the conflicting object is one that it already owns.
 CONTEXT:  SQL statement "CREATE COLLATION IF NOT EXISTS ext_cine_coll
@@ -412,6 +417,11 @@ extension script file "test_ext_cine--1.0.sql", near line 10
 DROP COLLATION ext_cine_coll;
 CREATE MATERIALIZED VIEW ext_cine_mv AS SELECT 11 AS f1;
 CREATE EXTENSION test_ext_cine;  -- fail
+WARNING:  IF NOT EXISTS is deprecated in materialized view creation
+LINE 1: CREATE MATERIALIZED VIEW IF NOT EXISTS ext_cine_mv AS SELECT...
+        ^
+HINT:  Use CREATE OR REPLACE MATERIALIZED VIEW ext_cine_mv.
+QUERY:  CREATE MATERIALIZED VIEW IF NOT EXISTS ext_cine_mv AS SELECT 42 AS f1;
 ERROR:  materialized view ext_cine_mv is not a member of extension "test_ext_cine"
 DETAIL:  An extension may only use CREATE ... IF NOT EXISTS to skip object creation if the conflicting object is one that it already owns.
 CONTEXT:  SQL statement "CREATE MATERIALIZED VIEW IF NOT EXISTS ext_cine_mv AS SELECT 42 AS f1"
@@ -420,6 +430,11 @@ DROP MATERIALIZED VIEW ext_cine_mv;
 CREATE FOREIGN DATA WRAPPER dummy;
 CREATE SERVER ext_cine_srv FOREIGN DATA WRAPPER dummy;
 CREATE EXTENSION test_ext_cine;  -- fail
+WARNING:  IF NOT EXISTS is deprecated in materialized view creation
+LINE 1: CREATE MATERIALIZED VIEW IF NOT EXISTS ext_cine_mv AS SELECT...
+        ^
+HINT:  Use CREATE OR REPLACE MATERIALIZED VIEW ext_cine_mv.
+QUERY:  CREATE MATERIALIZED VIEW IF NOT EXISTS ext_cine_mv AS SELECT 42 AS f1;
 ERROR:  server ext_cine_srv is not a member of extension "test_ext_cine"
 DETAIL:  An extension may only use CREATE ... IF NOT EXISTS to skip object creation if the conflicting object is one that it already owns.
 CONTEXT:  SQL statement "CREATE SERVER IF NOT EXISTS ext_cine_srv FOREIGN DATA WRAPPER ext_cine_fdw"
@@ -427,6 +442,11 @@ extension script file "test_ext_cine--1.0.sql", near line 17
 DROP SERVER ext_cine_srv;
 CREATE SCHEMA ext_cine_schema;
 CREATE EXTENSION test_ext_cine;  -- fail
+WARNING:  IF NOT EXISTS is deprecated in materialized view creation
+LINE 1: CREATE MATERIALIZED VIEW IF NOT EXISTS ext_cine_mv AS SELECT...
+        ^
+HINT:  Use CREATE OR REPLACE MATERIALIZED VIEW ext_cine_mv.
+QUERY:  CREATE MATERIALIZED VIEW IF NOT EXISTS ext_cine_mv AS SELECT 42 AS f1;
 ERROR:  schema ext_cine_schema is not a member of extension "test_ext_cine"
 DETAIL:  An extension may only use CREATE ... IF NOT EXISTS to skip object creation if the conflicting object is one that it already owns.
 CONTEXT:  SQL statement "CREATE SCHEMA IF NOT EXISTS ext_cine_schema"
@@ -434,6 +454,11 @@ extension script file "test_ext_cine--1.0.sql", near line 19
 DROP SCHEMA ext_cine_schema;
 CREATE SEQUENCE ext_cine_seq;
 CREATE EXTENSION test_ext_cine;  -- fail
+WARNING:  IF NOT EXISTS is deprecated in materialized view creation
+LINE 1: CREATE MATERIALIZED VIEW IF NOT EXISTS ext_cine_mv AS SELECT...
+        ^
+HINT:  Use CREATE OR REPLACE MATERIALIZED VIEW ext_cine_mv.
+QUERY:  CREATE MATERIALIZED VIEW IF NOT EXISTS ext_cine_mv AS SELECT 42 AS f1;
 ERROR:  sequence ext_cine_seq is not a member of extension "test_ext_cine"
 DETAIL:  An extension may only use CREATE ... IF NOT EXISTS to skip object creation if the conflicting object is one that it already owns.
 CONTEXT:  SQL statement "CREATE SEQUENCE IF NOT EXISTS ext_cine_seq"
@@ -441,6 +466,11 @@ extension script file "test_ext_cine--1.0.sql", near line 21
 DROP SEQUENCE ext_cine_seq;
 CREATE TABLE ext_cine_tab1 (x int);
 CREATE EXTENSION test_ext_cine;  -- fail
+WARNING:  IF NOT EXISTS is deprecated in materialized view creation
+LINE 1: CREATE MATERIALIZED VIEW IF NOT EXISTS ext_cine_mv AS SELECT...
+        ^
+HINT:  Use CREATE OR REPLACE MATERIALIZED VIEW ext_cine_mv.
+QUERY:  CREATE MATERIALIZED VIEW IF NOT EXISTS ext_cine_mv AS SELECT 42 AS f1;
 ERROR:  table ext_cine_tab1 is not a member of extension "test_ext_cine"
 DETAIL:  An extension may only use CREATE ... IF NOT EXISTS to skip object creation if the conflicting object is one that it already owns.
 CONTEXT:  SQL statement "CREATE TABLE IF NOT EXISTS ext_cine_tab1 (x int)"
@@ -448,12 +478,22 @@ extension script file "test_ext_cine--1.0.sql", near line 23
 DROP TABLE ext_cine_tab1;
 CREATE TABLE ext_cine_tab2 AS SELECT 42 AS y;
 CREATE EXTENSION test_ext_cine;  -- fail
+WARNING:  IF NOT EXISTS is deprecated in materialized view creation
+LINE 1: CREATE MATERIALIZED VIEW IF NOT EXISTS ext_cine_mv AS SELECT...
+        ^
+HINT:  Use CREATE OR REPLACE MATERIALIZED VIEW ext_cine_mv.
+QUERY:  CREATE MATERIALIZED VIEW IF NOT EXISTS ext_cine_mv AS SELECT 42 AS f1;
 ERROR:  table ext_cine_tab2 is not a member of extension "test_ext_cine"
 DETAIL:  An extension may only use CREATE ... IF NOT EXISTS to skip object creation if the conflicting object is one that it already owns.
 CONTEXT:  SQL statement "CREATE TABLE IF NOT EXISTS ext_cine_tab2 AS SELECT 42 AS y"
 extension script file "test_ext_cine--1.0.sql", near line 25
 DROP TABLE ext_cine_tab2;
 CREATE EXTENSION test_ext_cine;
+WARNING:  IF NOT EXISTS is deprecated in materialized view creation
+LINE 1: CREATE MATERIALIZED VIEW IF NOT EXISTS ext_cine_mv AS SELECT...
+        ^
+HINT:  Use CREATE OR REPLACE MATERIALIZED VIEW ext_cine_mv.
+QUERY:  CREATE MATERIALIZED VIEW IF NOT EXISTS ext_cine_mv AS SELECT 42 AS f1;
 \dx+ test_ext_cine
 Objects in extension "test_ext_cine"
         Object description         
@@ -475,6 +515,11 @@ Objects in extension "test_ext_cine"
 (14 rows)
 
 ALTER EXTENSION test_ext_cine UPDATE TO '1.1';
+WARNING:  IF NOT EXISTS is deprecated in materialized view creation
+LINE 1: CREATE MATERIALIZED VIEW IF NOT EXISTS ext_cine_mv AS SELECT...
+        ^
+HINT:  Use CREATE OR REPLACE MATERIALIZED VIEW ext_cine_mv.
+QUERY:  CREATE MATERIALIZED VIEW IF NOT EXISTS ext_cine_mv AS SELECT 42 AS f1;
 \dx+ test_ext_cine
 Objects in extension "test_ext_cine"
         Object description         
diff --git a/src/test/regress/expected/matview.out b/src/test/regress/expected/matview.out
index c0cf8ecbffc..d4b42da1ddf 100644
--- a/src/test/regress/expected/matview.out
+++ b/src/test/regress/expected/matview.out
@@ -565,6 +565,10 @@ CREATE MATERIALIZED VIEW mvtest_mv_foo AS SELECT * FROM mvtest_foo_data;
 CREATE MATERIALIZED VIEW mvtest_mv_foo AS SELECT * FROM mvtest_foo_data;
 ERROR:  relation "mvtest_mv_foo" already exists
 CREATE MATERIALIZED VIEW IF NOT EXISTS mvtest_mv_foo AS SELECT * FROM mvtest_foo_data;
+WARNING:  IF NOT EXISTS is deprecated in materialized view creation
+LINE 1: CREATE MATERIALIZED VIEW IF NOT EXISTS mvtest_mv_foo AS SELE...
+        ^
+HINT:  Use CREATE OR REPLACE MATERIALIZED VIEW mvtest_mv_foo.
 NOTICE:  relation "mvtest_mv_foo" already exists, skipping
 CREATE UNIQUE INDEX ON mvtest_mv_foo (i);
 RESET ROLE;
@@ -662,12 +666,20 @@ CREATE MATERIALIZED VIEW matview_ine_tab AS SELECT 1 / 0; -- error
 ERROR:  relation "matview_ine_tab" already exists
 CREATE MATERIALIZED VIEW IF NOT EXISTS matview_ine_tab AS
   SELECT 1 / 0; -- ok
+WARNING:  IF NOT EXISTS is deprecated in materialized view creation
+LINE 1: CREATE MATERIALIZED VIEW IF NOT EXISTS matview_ine_tab AS
+        ^
+HINT:  Use CREATE OR REPLACE MATERIALIZED VIEW matview_ine_tab.
 NOTICE:  relation "matview_ine_tab" already exists, skipping
 CREATE MATERIALIZED VIEW matview_ine_tab AS
   SELECT 1 / 0 WITH NO DATA; -- error
 ERROR:  relation "matview_ine_tab" already exists
 CREATE MATERIALIZED VIEW IF NOT EXISTS matview_ine_tab AS
   SELECT 1 / 0 WITH NO DATA; -- ok
+WARNING:  IF NOT EXISTS is deprecated in materialized view creation
+LINE 1: CREATE MATERIALIZED VIEW IF NOT EXISTS matview_ine_tab AS
+        ^
+HINT:  Use CREATE OR REPLACE MATERIALIZED VIEW matview_ine_tab.
 NOTICE:  relation "matview_ine_tab" already exists, skipping
 EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF)
   CREATE MATERIALIZED VIEW matview_ine_tab AS
@@ -676,6 +688,10 @@ ERROR:  relation "matview_ine_tab" already exists
 EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF)
   CREATE MATERIALIZED VIEW IF NOT EXISTS matview_ine_tab AS
     SELECT 1 / 0; -- ok
+WARNING:  IF NOT EXISTS is deprecated in materialized view creation
+LINE 2:   CREATE MATERIALIZED VIEW IF NOT EXISTS matview_ine_tab AS
+          ^
+HINT:  Use CREATE OR REPLACE MATERIALIZED VIEW matview_ine_tab.
 NOTICE:  relation "matview_ine_tab" already exists, skipping
  QUERY PLAN 
 ------------
@@ -688,6 +704,10 @@ ERROR:  relation "matview_ine_tab" already exists
 EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF)
   CREATE MATERIALIZED VIEW IF NOT EXISTS matview_ine_tab AS
     SELECT 1 / 0 WITH NO DATA; -- ok
+WARNING:  IF NOT EXISTS is deprecated in materialized view creation
+LINE 2:   CREATE MATERIALIZED VIEW IF NOT EXISTS matview_ine_tab AS
+          ^
+HINT:  Use CREATE OR REPLACE MATERIALIZED VIEW matview_ine_tab.
 NOTICE:  relation "matview_ine_tab" already exists, skipping
  QUERY PLAN 
 ------------
-- 
2.48.1
From eba3f5904ff97217a07628948fd3c707f5d9e94d Mon Sep 17 00:00:00 2001
From: Erik Wienhold <ewie@ewie.name>
Date: Fri, 26 Jul 2024 23:33:15 +0200
Subject: [PATCH v6 3/3] Replace matview WITH OLD DATA

---
 .../sgml/ref/create_materialized_view.sgml    | 16 +++++++++--
 src/backend/commands/createas.c               | 26 +++++++++++------
 src/backend/parser/gram.y                     | 16 +++++++++++
 src/include/nodes/primnodes.h                 |  1 +
 src/test/regress/expected/matview.out         | 28 +++++++++++++++++++
 src/test/regress/sql/matview.sql              | 15 ++++++++++
 6 files changed, 90 insertions(+), 12 deletions(-)

diff --git a/doc/src/sgml/ref/create_materialized_view.sgml b/doc/src/sgml/ref/create_materialized_view.sgml
index 5e03320eb73..1352e9de409 100644
--- a/doc/src/sgml/ref/create_materialized_view.sgml
+++ b/doc/src/sgml/ref/create_materialized_view.sgml
@@ -27,7 +27,7 @@ CREATE [ OR REPLACE ] MATERIALIZED VIEW [ IF NOT EXISTS ] <replaceable>table_nam
     [ WITH ( <replaceable class="parameter">storage_parameter</replaceable> [= <replaceable class="parameter">value</replaceable>] [, ... ] ) ]
     [ TABLESPACE <replaceable class="parameter">tablespace_name</replaceable> ]
     AS <replaceable>query</replaceable>
-    [ WITH [ NO ] DATA ]
+    [ WITH [ NO | OLD ] DATA ]
 </synopsis>
  </refsynopsisdiv>
 
@@ -37,7 +37,8 @@ CREATE [ OR REPLACE ] MATERIALIZED VIEW [ IF NOT EXISTS ] <replaceable>table_nam
   <para>
    <command>CREATE MATERIALIZED VIEW</command> defines a materialized view of
    a query.  The query is executed and used to populate the view at the time
-   the command is issued (unless <command>WITH NO DATA</command> is used) and may be
+   the command is issued (unless <command>WITH NO DATA</command> or
+   <command>WITH OLD DATA</command> is used) and may be
    refreshed later using <command>REFRESH MATERIALIZED VIEW</command>.
   </para>
 
@@ -162,7 +163,7 @@ CREATE [ OR REPLACE ] MATERIALIZED VIEW [ IF NOT EXISTS ] <replaceable>table_nam
    </varlistentry>
 
    <varlistentry>
-    <term><literal>WITH [ NO ] DATA</literal></term>
+    <term><literal>WITH [ NO | OLD ] DATA</literal></term>
     <listitem>
      <para>
       This clause specifies whether or not the materialized view should be
@@ -170,6 +171,15 @@ CREATE [ OR REPLACE ] MATERIALIZED VIEW [ IF NOT EXISTS ] <replaceable>table_nam
       flagged as unscannable and cannot be queried until <command>REFRESH
       MATERIALIZED VIEW</command> is used.
      </para>
+
+     <para>
+      The form <command>WITH OLD DATA</command> keeps the already stored data
+      when replacing an existing materialized view to keep it populated.  For
+      newly created materialized views, this has the same effect as
+      <command>WITH DATA</command>.  Use this form if you want to use
+      <command>REFRESH MATERIALIZED VIEW CONCURRENTLY</command> as it requires
+      a populated materialized view.
+     </para>
     </listitem>
    </varlistentry>
 
diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c
index 9f37ed7f177..0ed44b246bd 100644
--- a/src/backend/commands/createas.c
+++ b/src/backend/commands/createas.c
@@ -332,18 +332,26 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt,
 		/* An existing materialized view can be replaced. */
 		if (is_matview && into->replace)
 		{
-			RefreshMatViewStmt *refresh;
-
 			/* Change the relation to match the new query and other options. */
-			(void) create_ctas_nodata(query->targetList, into);
+			address = create_ctas_nodata(query->targetList, into);
 
-			/* Refresh the materialized view with a fake statement. */
-			refresh = makeNode(RefreshMatViewStmt);
-			refresh->relation = into->rel;
-			refresh->skipData = into->skipData;
-			refresh->concurrent = false;
+			/*
+			 * Refresh the materialized view with a fake statement unless we
+			 * must keep the old data.
+			 */
+			if (!into->keepData)
+			{
+				RefreshMatViewStmt *refresh;
+
+				refresh = makeNode(RefreshMatViewStmt);
+				refresh->relation = into->rel;
+				refresh->skipData = into->skipData;
+				refresh->concurrent = false;
+
+				address = ExecRefreshMatView(refresh, pstate->p_sourcetext, qc);
+			}
 
-			return ExecRefreshMatView(refresh, pstate->p_sourcetext, qc);
+			return address;
 		}
 
 		return InvalidObjectAddress;
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 8be790eaccf..e405268f413 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -4924,6 +4924,22 @@ CreateMatViewStmt:
 					$7->replace = true;
 					$$ = (Node *) ctas;
 				}
+		| CREATE OR REPLACE OptNoLog MATERIALIZED VIEW create_mv_target AS SelectStmt WITH OLD DATA_P
+				{
+					CreateTableAsStmt *ctas = makeNode(CreateTableAsStmt);
+
+					ctas->query = $9;
+					ctas->into = $7;
+					ctas->objtype = OBJECT_MATVIEW;
+					ctas->is_select_into = false;
+					ctas->if_not_exists = false;
+					/* cram additional flags into the IntoClause */
+					$7->rel->relpersistence = $4;
+					$7->skipData = false;
+					$7->keepData = true;
+					$7->replace = true;
+					$$ = (Node *) ctas;
+				}
 		;
 
 create_mv_target:
diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h
index e800d13a6e0..5290e6c0e8a 100644
--- a/src/include/nodes/primnodes.h
+++ b/src/include/nodes/primnodes.h
@@ -169,6 +169,7 @@ typedef struct IntoClause
 	/* materialized view's SELECT query */
 	struct Query *viewQuery pg_node_attr(query_jumble_ignore);
 	bool		skipData;		/* true for WITH NO DATA */
+	bool		keepData;		/* true for WITH OLD DATA */
 	bool		replace;		/* replace existing matview? */
 } IntoClause;
 
diff --git a/src/test/regress/expected/matview.out b/src/test/regress/expected/matview.out
index d4b42da1ddf..f0f52dd1c6c 100644
--- a/src/test/regress/expected/matview.out
+++ b/src/test/regress/expected/matview.out
@@ -751,6 +751,23 @@ SELECT * FROM mvtest_replace;
  3
 (1 row)
 
+-- replace query but keep old data
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 5 AS a
+  WITH OLD DATA;
+SELECT * FROM mvtest_replace;
+ a 
+---
+ 3
+(1 row)
+
+REFRESH MATERIALIZED VIEW mvtest_replace;
+SELECT * FROM mvtest_replace;
+ a 
+---
+ 5
+(1 row)
+
 -- add column
 CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
   SELECT 4 AS a, 1 b;
@@ -905,3 +922,14 @@ ERROR:  syntax error at or near "NOT"
 LINE 1: CREATE OR REPLACE MATERIALIZED VIEW IF NOT EXISTS mvtest_rep...
                                                ^
 DROP MATERIALIZED VIEW mvtest_replace;
+-- Create new matview WITH OLD DATA.  This populates the new matview as if
+-- WITH DATA had been specified.
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 17 AS a
+  WITH OLD DATA;
+SELECT * FROM mvtest_replace;
+ a  
+----
+ 17
+(1 row)
+
diff --git a/src/test/regress/sql/matview.sql b/src/test/regress/sql/matview.sql
index d6a4dc4b857..91f547e9cb8 100644
--- a/src/test/regress/sql/matview.sql
+++ b/src/test/regress/sql/matview.sql
@@ -338,6 +338,14 @@ SELECT * FROM mvtest_replace; -- error: not populated
 REFRESH MATERIALIZED VIEW mvtest_replace;
 SELECT * FROM mvtest_replace;
 
+-- replace query but keep old data
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 5 AS a
+  WITH OLD DATA;
+SELECT * FROM mvtest_replace;
+REFRESH MATERIALIZED VIEW mvtest_replace;
+SELECT * FROM mvtest_replace;
+
 -- add column
 CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
   SELECT 4 AS a, 1 b;
@@ -422,3 +430,10 @@ CREATE OR REPLACE MATERIALIZED VIEW IF NOT EXISTS mvtest_replace AS
   SELECT 1 AS a;
 
 DROP MATERIALIZED VIEW mvtest_replace;
+
+-- Create new matview WITH OLD DATA.  This populates the new matview as if
+-- WITH DATA had been specified.
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 17 AS a
+  WITH OLD DATA;
+SELECT * FROM mvtest_replace;
-- 
2.48.1

Attachments:

  [text/plain] v6-0001-Add-CREATE-OR-REPLACE-MATERIALIZED-VIEW.patch (33.1K, ../../bf64dad9-eaaa-4911-b447-13acee89ea80@ewie.name/2-v6-0001-Add-CREATE-OR-REPLACE-MATERIALIZED-VIEW.patch)
  download | inline diff:
From 6ec126d8da5ca80f93ea8a58e07d654f5e21ef6d Mon Sep 17 00:00:00 2001
From: Erik Wienhold <ewie@ewie.name>
Date: Tue, 21 May 2024 18:35:47 +0200
Subject: [PATCH v6 1/3] Add CREATE OR REPLACE MATERIALIZED VIEW

---
 .../sgml/ref/create_materialized_view.sgml    |  15 +-
 src/backend/commands/createas.c               | 207 ++++++++++++++----
 src/backend/commands/tablecmds.c              |   8 +-
 src/backend/commands/view.c                   | 106 ++++++---
 src/backend/parser/gram.y                     |  15 ++
 src/bin/psql/tab-complete.in.c                |  26 ++-
 src/include/commands/view.h                   |   3 +
 src/include/nodes/parsenodes.h                |   2 +-
 src/include/nodes/primnodes.h                 |   1 +
 src/test/regress/expected/matview.out         | 191 ++++++++++++++++
 src/test/regress/sql/matview.sql              | 108 +++++++++
 11 files changed, 589 insertions(+), 93 deletions(-)

diff --git a/doc/src/sgml/ref/create_materialized_view.sgml b/doc/src/sgml/ref/create_materialized_view.sgml
index 62d897931c3..5e03320eb73 100644
--- a/doc/src/sgml/ref/create_materialized_view.sgml
+++ b/doc/src/sgml/ref/create_materialized_view.sgml
@@ -21,7 +21,7 @@ PostgreSQL documentation
 
  <refsynopsisdiv>
 <synopsis>
-CREATE MATERIALIZED VIEW [ IF NOT EXISTS ] <replaceable>table_name</replaceable>
+CREATE [ OR REPLACE ] MATERIALIZED VIEW [ IF NOT EXISTS ] <replaceable>table_name</replaceable>
     [ (<replaceable>column_name</replaceable> [, ...] ) ]
     [ USING <replaceable class="parameter">method</replaceable> ]
     [ WITH ( <replaceable class="parameter">storage_parameter</replaceable> [= <replaceable class="parameter">value</replaceable>] [, ... ] ) ]
@@ -60,6 +60,17 @@ CREATE MATERIALIZED VIEW [ IF NOT EXISTS ] <replaceable>table_name</replaceable>
   <title>Parameters</title>
 
   <variablelist>
+   <varlistentry>
+    <term><literal>OR REPLACE</literal></term>
+    <listitem>
+     <para>
+      Replaces a materialized view if it already exists.
+      Specifying <literal>OR REPLACE</literal> together with
+      <literal>IF NOT EXISTS</literal> is an error.
+     </para>
+    </listitem>
+   </varlistentry>
+
    <varlistentry>
     <term><literal>IF NOT EXISTS</literal></term>
     <listitem>
@@ -67,7 +78,7 @@ CREATE MATERIALIZED VIEW [ IF NOT EXISTS ] <replaceable>table_name</replaceable>
       Do not throw an error if a materialized view with the same name already
       exists. A notice is issued in this case.  Note that there is no guarantee
       that the existing materialized view is anything like the one that would
-      have been created.
+      have been created, unless you use <literal>OR REPLACE</literal> instead.
      </para>
     </listitem>
    </varlistentry>
diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c
index 44b4665ccd3..9f37ed7f177 100644
--- a/src/backend/commands/createas.c
+++ b/src/backend/commands/createas.c
@@ -79,55 +79,151 @@ static void intorel_destroy(DestReceiver *self);
 static ObjectAddress
 create_ctas_internal(List *attrList, IntoClause *into)
 {
-	CreateStmt *create = makeNode(CreateStmt);
-	bool		is_matview;
+	bool		is_matview,
+				replace = false;
 	char		relkind;
-	Datum		toast_options;
-	const char *const validnsps[] = HEAP_RELOPT_NAMESPACES;
+	Oid			matviewOid = InvalidOid;
 	ObjectAddress intoRelationAddr;
 
 	/* This code supports both CREATE TABLE AS and CREATE MATERIALIZED VIEW */
 	is_matview = (into->viewQuery != NULL);
 	relkind = is_matview ? RELKIND_MATVIEW : RELKIND_RELATION;
 
-	/*
-	 * Create the target relation by faking up a CREATE TABLE parsetree and
-	 * passing it to DefineRelation.
-	 */
-	create->relation = into->rel;
-	create->tableElts = attrList;
-	create->inhRelations = NIL;
-	create->ofTypename = NULL;
-	create->constraints = NIL;
-	create->options = into->options;
-	create->oncommit = into->onCommit;
-	create->tablespacename = into->tableSpaceName;
-	create->if_not_exists = false;
-	create->accessMethod = into->accessMethod;
+	/* Check if an existing materialized view needs to be replaced. */
+	if (is_matview)
+	{
+		LOCKMODE	lockmode;
 
-	/*
-	 * Create the relation.  (This will error out if there's an existing view,
-	 * so we don't need more code to complain if "replace" is false.)
-	 */
-	intoRelationAddr = DefineRelation(create, relkind, InvalidOid, NULL, NULL);
+		lockmode = into->replace ? AccessExclusiveLock : NoLock;
+		(void) RangeVarGetAndCheckCreationNamespace(into->rel, lockmode,
+													&matviewOid);
+		replace = OidIsValid(matviewOid) && into->replace;
+	}
 
-	/*
-	 * If necessary, create a TOAST table for the target table.  Note that
-	 * NewRelationCreateToastTable ends with CommandCounterIncrement(), so
-	 * that the TOAST table will be visible for insertion.
-	 */
-	CommandCounterIncrement();
+	if (is_matview && replace)
+	{
+		Relation	rel;
+		List	   *atcmds = NIL;
+		AlterTableCmd *atcmd;
+		TupleDesc	descriptor;
+
+		rel = relation_open(matviewOid, NoLock);
+
+		if (rel->rd_rel->relkind != RELKIND_MATVIEW)
+			ereport(ERROR,
+					errcode(ERRCODE_WRONG_OBJECT_TYPE),
+					errmsg("\"%s\" is not a materialized view",
+						   RelationGetRelationName(rel)));
+
+		CheckTableNotInUse(rel, "CREATE OR REPLACE MATERIALIZED VIEW");
+
+		descriptor = BuildDescForRelation(attrList);
+		checkViewColumns(descriptor, rel->rd_att, true);
+
+		/* Add new attributes via ALTER TABLE. */
+		if (list_length(attrList) > rel->rd_att->natts)
+		{
+			ListCell   *c;
+			int			skip = rel->rd_att->natts;
+
+			foreach(c, attrList)
+			{
+				if (skip > 0)
+				{
+					skip--;
+					continue;
+				}
+				atcmd = makeNode(AlterTableCmd);
+				atcmd->subtype = AT_AddColumnToView;
+				atcmd->def = (Node *) lfirst(c);
+				atcmds = lappend(atcmds, atcmd);
+			}
+		}
+
+		/* Set access method via ALTER TABLE. */
+		if (into->accessMethod != NULL)
+		{
+			atcmd = makeNode(AlterTableCmd);
+			atcmd->subtype = AT_SetAccessMethod;
+			atcmd->name = into->accessMethod;
+			atcmds = lappend(atcmds, atcmd);
+		}
+
+		/* Set tablespace via ALTER TABLE. */
+		if (into->tableSpaceName != NULL)
+		{
+			atcmd = makeNode(AlterTableCmd);
+			atcmd->subtype = AT_SetTableSpace;
+			atcmd->name = into->tableSpaceName;
+			atcmds = lappend(atcmds, atcmd);
+		}
+
+		/* Set storage parameters via ALTER TABLE. */
+		if (into->options != NIL)
+		{
+			atcmd = makeNode(AlterTableCmd);
+			atcmd->subtype = AT_ReplaceRelOptions;
+			atcmd->def = (Node *) into->options;
+			atcmds = lappend(atcmds, atcmd);
+		}
+
+		if (atcmds != NIL)
+		{
+			AlterTableInternal(matviewOid, atcmds, true);
+			CommandCounterIncrement();
+		}
+
+		relation_close(rel, NoLock);
+
+		ObjectAddressSet(intoRelationAddr, RelationRelationId, matviewOid);
+	}
+	else
+	{
+		CreateStmt *create = makeNode(CreateStmt);
+		Datum		toast_options;
+		const static char *validnsps[] = HEAP_RELOPT_NAMESPACES;
+
+		/*
+		 * Create the target relation by faking up a CREATE TABLE parsetree
+		 * and passing it to DefineRelation.
+		 */
+		create->relation = into->rel;
+		create->tableElts = attrList;
+		create->inhRelations = NIL;
+		create->ofTypename = NULL;
+		create->constraints = NIL;
+		create->options = into->options;
+		create->oncommit = into->onCommit;
+		create->tablespacename = into->tableSpaceName;
+		create->if_not_exists = false;
+		create->accessMethod = into->accessMethod;
+
+		/*
+		 * Create the relation.  (This will error out if there's an existing
+		 * view, so we don't need more code to complain if "replace" is
+		 * false.)
+		 */
+		intoRelationAddr = DefineRelation(create, relkind, InvalidOid, NULL,
+										  NULL);
 
-	/* parse and validate reloptions for the toast table */
-	toast_options = transformRelOptions((Datum) 0,
-										create->options,
-										"toast",
-										validnsps,
-										true, false);
+		/*
+		 * If necessary, create a TOAST table for the target table.  Note that
+		 * NewRelationCreateToastTable ends with CommandCounterIncrement(), so
+		 * that the TOAST table will be visible for insertion.
+		 */
+		CommandCounterIncrement();
+
+		/* parse and validate reloptions for the toast table */
+		toast_options = transformRelOptions((Datum) 0,
+											create->options,
+											"toast",
+											validnsps,
+											true, false);
 
-	(void) heap_reloptions(RELKIND_TOASTVALUE, toast_options, true);
+		(void) heap_reloptions(RELKIND_TOASTVALUE, toast_options, true);
 
-	NewRelationCreateToastTable(intoRelationAddr.objectId, toast_options);
+		NewRelationCreateToastTable(intoRelationAddr.objectId, toast_options);
+	}
 
 	/* Create the "view" part of a materialized view. */
 	if (is_matview)
@@ -135,7 +231,7 @@ create_ctas_internal(List *attrList, IntoClause *into)
 		/* StoreViewQuery scribbles on tree, so make a copy */
 		Query	   *query = copyObject(into->viewQuery);
 
-		StoreViewQuery(intoRelationAddr.objectId, query, false);
+		StoreViewQuery(intoRelationAddr.objectId, query, replace);
 		CommandCounterIncrement();
 	}
 
@@ -232,7 +328,26 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt,
 
 	/* Check if the relation exists or not */
 	if (CreateTableAsRelExists(stmt))
+	{
+		/* An existing materialized view can be replaced. */
+		if (is_matview && into->replace)
+		{
+			RefreshMatViewStmt *refresh;
+
+			/* Change the relation to match the new query and other options. */
+			(void) create_ctas_nodata(query->targetList, into);
+
+			/* Refresh the materialized view with a fake statement. */
+			refresh = makeNode(RefreshMatViewStmt);
+			refresh->relation = into->rel;
+			refresh->skipData = into->skipData;
+			refresh->concurrent = false;
+
+			return ExecRefreshMatView(refresh, pstate->p_sourcetext, qc);
+		}
+
 		return InvalidObjectAddress;
+	}
 
 	/*
 	 * Create the tuple receiver object and insert info it will need
@@ -401,14 +516,15 @@ CreateTableAsRelExists(CreateTableAsStmt *ctas)
 	oldrelid = get_relname_relid(into->rel->relname, nspid);
 	if (OidIsValid(oldrelid))
 	{
-		if (!ctas->if_not_exists)
+		if (!ctas->if_not_exists && !into->replace)
 			ereport(ERROR,
 					(errcode(ERRCODE_DUPLICATE_TABLE),
 					 errmsg("relation \"%s\" already exists",
 							into->rel->relname)));
 
 		/*
-		 * The relation exists and IF NOT EXISTS has been specified.
+		 * The relation exists and IF NOT EXISTS or OR REPLACE has been
+		 * specified.
 		 *
 		 * If we are in an extension script, insist that the pre-existing
 		 * object be a member of the extension, to avoid security risks.
@@ -416,11 +532,12 @@ CreateTableAsRelExists(CreateTableAsStmt *ctas)
 		ObjectAddressSet(address, RelationRelationId, oldrelid);
 		checkMembershipInCurrentExtension(&address);
 
-		/* OK to skip */
-		ereport(NOTICE,
-				(errcode(ERRCODE_DUPLICATE_TABLE),
-				 errmsg("relation \"%s\" already exists, skipping",
-						into->rel->relname)));
+		if (ctas->if_not_exists)
+			/* OK to skip */
+			ereport(NOTICE,
+					(errcode(ERRCODE_DUPLICATE_TABLE),
+					 errmsg("relation \"%s\" already exists, skipping",
+							into->rel->relname)));
 		return true;
 	}
 
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 59156a1c1f6..49713bc0422 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -4605,7 +4605,7 @@ AlterTableGetLockLevel(List *cmds)
 				 * Subcommands that may be visible to concurrent SELECTs
 				 */
 			case AT_DropColumn: /* change visible to SELECT */
-			case AT_AddColumnToView:	/* CREATE VIEW */
+			case AT_AddColumnToView:	/* via CREATE OR REPLACE [MATERIALIZED] VIEW */
 			case AT_DropOids:	/* used to equiv to DropColumn */
 			case AT_EnableAlwaysRule:	/* may change SELECT rules */
 			case AT_EnableReplicaRule:	/* may change SELECT rules */
@@ -4900,8 +4900,8 @@ ATPrepCmd(List **wqueue, Relation rel, AlterTableCmd *cmd,
 			/* Recursion occurs during execution phase */
 			pass = AT_PASS_ADD_COL;
 			break;
-		case AT_AddColumnToView:	/* add column via CREATE OR REPLACE VIEW */
-			ATSimplePermissions(cmd->subtype, rel, ATT_VIEW);
+		case AT_AddColumnToView:	/* via CREATE OR REPLACE [MATERIALIZED] VIEW */
+			ATSimplePermissions(cmd->subtype, rel, ATT_VIEW | ATT_MATVIEW);
 			ATPrepAddColumn(wqueue, rel, recurse, recursing, true, cmd,
 							lockmode, context);
 			/* Recursion occurs during execution phase */
@@ -5332,7 +5332,7 @@ ATExecCmd(List **wqueue, AlteredTableInfo *tab,
 	switch (cmd->subtype)
 	{
 		case AT_AddColumn:		/* ADD COLUMN */
-		case AT_AddColumnToView:	/* add column via CREATE OR REPLACE VIEW */
+		case AT_AddColumnToView:	/* via CREATE OR REPLACE [MATERIALIZED] VIEW */
 			address = ATExecAddColumn(wqueue, tab, rel, &cmd,
 									  cmd->recurse, false,
 									  lockmode, cur_pass, context);
diff --git a/src/backend/commands/view.c b/src/backend/commands/view.c
index 6f0301555e0..53d4cacc4c1 100644
--- a/src/backend/commands/view.c
+++ b/src/backend/commands/view.c
@@ -30,8 +30,6 @@
 #include "utils/lsyscache.h"
 #include "utils/rel.h"
 
-static void checkViewColumns(TupleDesc newdesc, TupleDesc olddesc);
-
 /*---------------------------------------------------------------------
  * DefineVirtualRelation
  *
@@ -129,7 +127,7 @@ DefineVirtualRelation(RangeVar *relation, List *tlist, bool replace,
 		 * column list.
 		 */
 		descriptor = BuildDescForRelation(attrList);
-		checkViewColumns(descriptor, rel->rd_att);
+		checkViewColumns(descriptor, rel->rd_att, false);
 
 		/*
 		 * If new attributes have been added, we must add pg_attribute entries
@@ -263,15 +261,22 @@ DefineVirtualRelation(RangeVar *relation, List *tlist, bool replace,
  * added to generate specific complaints.  Also, we allow the new view to have
  * more columns than the old.
  */
-static void
-checkViewColumns(TupleDesc newdesc, TupleDesc olddesc)
+void
+checkViewColumns(TupleDesc newdesc, TupleDesc olddesc, bool is_matview)
 {
 	int			i;
 
 	if (newdesc->natts < olddesc->natts)
-		ereport(ERROR,
-				(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
-				 errmsg("cannot drop columns from view")));
+	{
+		if (is_matview)
+			ereport(ERROR,
+					errcode(ERRCODE_INVALID_TABLE_DEFINITION),
+					errmsg("cannot drop columns from materialized view"));
+		else
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
+					 errmsg("cannot drop columns from view")));
+	}
 
 	for (i = 0; i < olddesc->natts; i++)
 	{
@@ -280,17 +285,34 @@ checkViewColumns(TupleDesc newdesc, TupleDesc olddesc)
 
 		/* XXX msg not right, but we don't support DROP COL on view anyway */
 		if (newattr->attisdropped != oldattr->attisdropped)
-			ereport(ERROR,
-					(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
-					 errmsg("cannot drop columns from view")));
+		{
+			if (is_matview)
+				ereport(ERROR,
+						errcode(ERRCODE_INVALID_TABLE_DEFINITION),
+						errmsg("cannot drop columns from materialized view"));
+			else
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
+						 errmsg("cannot drop columns from view")));
+		}
 
 		if (strcmp(NameStr(newattr->attname), NameStr(oldattr->attname)) != 0)
-			ereport(ERROR,
-					(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
-					 errmsg("cannot change name of view column \"%s\" to \"%s\"",
-							NameStr(oldattr->attname),
-							NameStr(newattr->attname)),
-					 errhint("Use ALTER VIEW ... RENAME COLUMN ... to change name of view column instead.")));
+		{
+			if (is_matview)
+				ereport(ERROR,
+						errcode(ERRCODE_INVALID_TABLE_DEFINITION),
+						errmsg("cannot change name of materialized view column \"%s\" to \"%s\"",
+							   NameStr(oldattr->attname),
+							   NameStr(newattr->attname)),
+						errhint("Use ALTER MATERIALIZED VIEW ... RENAME COLUMN ... to change name of materialized view column instead."));
+			else
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
+						 errmsg("cannot change name of view column \"%s\" to \"%s\"",
+								NameStr(oldattr->attname),
+								NameStr(newattr->attname)),
+						 errhint("Use ALTER VIEW ... RENAME COLUMN ... to change name of view column instead.")));
+		}
 
 		/*
 		 * We cannot allow type, typmod, or collation to change, since these
@@ -299,26 +321,48 @@ checkViewColumns(TupleDesc newdesc, TupleDesc olddesc)
 		 */
 		if (newattr->atttypid != oldattr->atttypid ||
 			newattr->atttypmod != oldattr->atttypmod)
-			ereport(ERROR,
-					(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
-					 errmsg("cannot change data type of view column \"%s\" from %s to %s",
-							NameStr(oldattr->attname),
-							format_type_with_typemod(oldattr->atttypid,
-													 oldattr->atttypmod),
-							format_type_with_typemod(newattr->atttypid,
-													 newattr->atttypmod))));
+		{
+			if (is_matview)
+				ereport(ERROR,
+						errcode(ERRCODE_INVALID_TABLE_DEFINITION),
+						errmsg("cannot change data type of materialized view column \"%s\" from %s to %s",
+							   NameStr(oldattr->attname),
+							   format_type_with_typemod(oldattr->atttypid,
+														oldattr->atttypmod),
+							   format_type_with_typemod(newattr->atttypid,
+														newattr->atttypmod)));
+			else
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
+						 errmsg("cannot change data type of view column \"%s\" from %s to %s",
+								NameStr(oldattr->attname),
+								format_type_with_typemod(oldattr->atttypid,
+														 oldattr->atttypmod),
+								format_type_with_typemod(newattr->atttypid,
+														 newattr->atttypmod))));
+		}
 
 		/*
 		 * At this point, attcollations should be both valid or both invalid,
 		 * so applying get_collation_name unconditionally should be fine.
 		 */
 		if (newattr->attcollation != oldattr->attcollation)
-			ereport(ERROR,
-					(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
-					 errmsg("cannot change collation of view column \"%s\" from \"%s\" to \"%s\"",
-							NameStr(oldattr->attname),
-							get_collation_name(oldattr->attcollation),
-							get_collation_name(newattr->attcollation))));
+		{
+			if (is_matview)
+				ereport(ERROR,
+						errcode(ERRCODE_INVALID_TABLE_DEFINITION),
+						errmsg("cannot change collation of materialized view column \"%s\" from \"%s\" to \"%s\"",
+							   NameStr(oldattr->attname),
+							   get_collation_name(oldattr->attcollation),
+							   get_collation_name(newattr->attcollation)));
+			else
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
+						 errmsg("cannot change collation of view column \"%s\" from \"%s\" to \"%s\"",
+								NameStr(oldattr->attname),
+								get_collation_name(oldattr->attcollation),
+								get_collation_name(newattr->attcollation))));
+		}
 	}
 
 	/*
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 271ae26cbaf..d2edc460378 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -4895,6 +4895,21 @@ CreateMatViewStmt:
 					$8->skipData = !($11);
 					$$ = (Node *) ctas;
 				}
+		| CREATE OR REPLACE OptNoLog MATERIALIZED VIEW create_mv_target AS SelectStmt opt_with_data
+				{
+					CreateTableAsStmt *ctas = makeNode(CreateTableAsStmt);
+
+					ctas->query = $9;
+					ctas->into = $7;
+					ctas->objtype = OBJECT_MATVIEW;
+					ctas->is_select_into = false;
+					ctas->if_not_exists = false;
+					/* cram additional flags into the IntoClause */
+					$7->rel->relpersistence = $4;
+					$7->skipData = !($10);
+					$7->replace = true;
+					$$ = (Node *) ctas;
+				}
 		;
 
 create_mv_target:
diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c
index 8432be641ac..e862b135a73 100644
--- a/src/bin/psql/tab-complete.in.c
+++ b/src/bin/psql/tab-complete.in.c
@@ -2162,7 +2162,7 @@ match_previous_words(int pattern_id,
 	/* complete with something you can create or replace */
 	else if (TailMatches("CREATE", "OR", "REPLACE"))
 		COMPLETE_WITH("FUNCTION", "PROCEDURE", "LANGUAGE", "RULE", "VIEW",
-					  "AGGREGATE", "TRANSFORM", "TRIGGER");
+					  "AGGREGATE", "TRANSFORM", "TRIGGER", "MATERIALIZED VIEW");
 
 /* DROP, but not DROP embedded in other commands */
 	/* complete with something you can drop */
@@ -4008,28 +4008,34 @@ match_previous_words(int pattern_id,
 		COMPLETE_WITH("SELECT");
 
 /* CREATE MATERIALIZED VIEW */
-	else if (Matches("CREATE", "MATERIALIZED"))
+	else if (Matches("CREATE", "MATERIALIZED") ||
+			 Matches("CREATE", "OR", "REPLACE", "MATERIALIZED"))
 		COMPLETE_WITH("VIEW");
-	/* Complete CREATE MATERIALIZED VIEW <name> with AS or USING */
-	else if (Matches("CREATE", "MATERIALIZED", "VIEW", MatchAny))
+	/* Complete CREATE [ OR REPLACE ] MATERIALIZED VIEW <name> with AS or USING */
+	else if (Matches("CREATE", "MATERIALIZED", "VIEW", MatchAny) ||
+			 Matches("CREATE", "OR", "REPLACE", "MATERIALIZED", "VIEW", MatchAny))
 		COMPLETE_WITH("AS", "USING");
 
 	/*
-	 * Complete CREATE MATERIALIZED VIEW <name> USING with list of access
+	 * Complete CREATE [ OR REPLACE ] MATERIALIZED VIEW <name> USING with list of access
 	 * methods
 	 */
-	else if (Matches("CREATE", "MATERIALIZED", "VIEW", MatchAny, "USING"))
+	else if (Matches("CREATE", "MATERIALIZED", "VIEW", MatchAny, "USING") ||
+			 Matches("CREATE", "OR", "REPLACE", "MATERIALIZED", "VIEW", MatchAny, "USING"))
 		COMPLETE_WITH_QUERY(Query_for_list_of_table_access_methods);
-	/* Complete CREATE MATERIALIZED VIEW <name> USING <access method> with AS */
-	else if (Matches("CREATE", "MATERIALIZED", "VIEW", MatchAny, "USING", MatchAny))
+	/* Complete CREATE [ OR REPLACE ] MATERIALIZED VIEW <name> USING <access method> with AS */
+	else if (Matches("CREATE", "MATERIALIZED", "VIEW", MatchAny, "USING", MatchAny) ||
+			 Matches("CREATE", "OR", "REPLACE", "MATERIALIZED", "VIEW", MatchAny, "USING", MatchAny))
 		COMPLETE_WITH("AS");
 
 	/*
-	 * Complete CREATE MATERIALIZED VIEW <name> [USING <access method> ] AS
+	 * Complete CREATE [ OR REPLACE ] MATERIALIZED VIEW <name> [USING <access method> ] AS
 	 * with "SELECT"
 	 */
 	else if (Matches("CREATE", "MATERIALIZED", "VIEW", MatchAny, "AS") ||
-			 Matches("CREATE", "MATERIALIZED", "VIEW", MatchAny, "USING", MatchAny, "AS"))
+			 Matches("CREATE", "OR", "REPLACE", "MATERIALIZED", "VIEW", MatchAny, "AS") ||
+			 Matches("CREATE", "MATERIALIZED", "VIEW", MatchAny, "USING", MatchAny, "AS") ||
+			 Matches("CREATE", "OR", "REPLACE", "MATERIALIZED", "VIEW", MatchAny, "USING", MatchAny, "AS"))
 		COMPLETE_WITH("SELECT");
 
 /* CREATE EVENT TRIGGER */
diff --git a/src/include/commands/view.h b/src/include/commands/view.h
index c41f51b161c..95290f0a1c4 100644
--- a/src/include/commands/view.h
+++ b/src/include/commands/view.h
@@ -22,4 +22,7 @@ extern ObjectAddress DefineView(ViewStmt *stmt, const char *queryString,
 
 extern void StoreViewQuery(Oid viewOid, Query *viewParse, bool replace);
 
+extern void checkViewColumns(TupleDesc newdesc, TupleDesc olddesc,
+							 bool is_matview);
+
 #endif							/* VIEW_H */
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 23c9e3c5abf..5a25ba96ab3 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -2402,7 +2402,7 @@ typedef struct AlterTableStmt
 typedef enum AlterTableType
 {
 	AT_AddColumn,				/* add column */
-	AT_AddColumnToView,			/* implicitly via CREATE OR REPLACE VIEW */
+	AT_AddColumnToView,			/* implicitly via CREATE OR REPLACE [MATERIALIZED] VIEW */
 	AT_ColumnDefault,			/* alter column default */
 	AT_CookedColumnDefault,		/* add a pre-cooked column default */
 	AT_DropNotNull,				/* alter column drop not null */
diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h
index d0576da3e25..e800d13a6e0 100644
--- a/src/include/nodes/primnodes.h
+++ b/src/include/nodes/primnodes.h
@@ -169,6 +169,7 @@ typedef struct IntoClause
 	/* materialized view's SELECT query */
 	struct Query *viewQuery pg_node_attr(query_jumble_ignore);
 	bool		skipData;		/* true for WITH NO DATA */
+	bool		replace;		/* replace existing matview? */
 } IntoClause;
 
 
diff --git a/src/test/regress/expected/matview.out b/src/test/regress/expected/matview.out
index 54939ecc6b0..c0cf8ecbffc 100644
--- a/src/test/regress/expected/matview.out
+++ b/src/test/regress/expected/matview.out
@@ -694,3 +694,194 @@ NOTICE:  relation "matview_ine_tab" already exists, skipping
 (0 rows)
 
 DROP MATERIALIZED VIEW matview_ine_tab;
+--
+-- test CREATE OR REPLACE MATERIALIZED VIEW
+--
+-- matview does not already exist
+DROP MATERIALIZED VIEW IF EXISTS mvtest_replace;
+NOTICE:  materialized view "mvtest_replace" does not exist, skipping
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 1 AS a;
+SELECT * FROM mvtest_replace;
+ a 
+---
+ 1
+(1 row)
+
+-- replace query with data
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 2 AS a;
+SELECT * FROM mvtest_replace;
+ a 
+---
+ 2
+(1 row)
+
+-- replace query without data
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 3 AS a
+  WITH NO DATA;
+SELECT * FROM mvtest_replace; -- error: not populated
+ERROR:  materialized view "mvtest_replace" has not been populated
+HINT:  Use the REFRESH MATERIALIZED VIEW command.
+REFRESH MATERIALIZED VIEW mvtest_replace;
+SELECT * FROM mvtest_replace;
+ a 
+---
+ 3
+(1 row)
+
+-- add column
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 4 AS a, 1 b;
+SELECT * FROM mvtest_replace;
+ a | b 
+---+---
+ 4 | 1
+(1 row)
+
+-- replace table options
+SELECT m.*, c.relname, c.reloptions, s.spcname, a.amname
+  FROM mvtest_replace m
+    CROSS JOIN pg_class c
+    LEFT JOIN pg_tablespace s ON s.oid = c.reltablespace
+    LEFT JOIN pg_am a ON a.oid = c.relam
+  WHERE c.relname = 'mvtest_replace';
+ a | b |    relname     | reloptions | spcname | amname 
+---+---+----------------+------------+---------+--------
+ 4 | 1 | mvtest_replace |            |         | heap
+(1 row)
+
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace
+  USING heap2
+  WITH (fillfactor = 50)
+  TABLESPACE regress_tblspace
+  AS SELECT 5 AS a, 1 AS b;
+SELECT m.*, c.relname, c.reloptions, s.spcname, a.amname
+  FROM mvtest_replace m
+    CROSS JOIN pg_class c
+    LEFT JOIN pg_tablespace s ON s.oid = c.reltablespace
+    LEFT JOIN pg_am a ON a.oid = c.relam
+  WHERE c.relname = 'mvtest_replace';
+ a | b |    relname     |   reloptions    |     spcname      | amname 
+---+---+----------------+-----------------+------------------+--------
+ 5 | 1 | mvtest_replace | {fillfactor=50} | regress_tblspace | heap2
+(1 row)
+
+-- can replace matview that has a dependent view
+CREATE VIEW mvtest_replace_v AS
+  SELECT * FROM mvtest_replace;
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 6 AS a, 1 AS b;
+SELECT * FROM mvtest_replace, mvtest_replace_v;
+ a | b | a | b 
+---+---+---+---
+ 6 | 1 | 6 | 1
+(1 row)
+
+DROP VIEW mvtest_replace_v;
+-- index gets rebuilt when replacing with data
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 7 AS a, 1 AS b;
+CREATE UNIQUE INDEX ON mvtest_replace (b);
+SELECT * FROM mvtest_replace;
+ a | b 
+---+---
+ 7 | 1
+(1 row)
+
+SET enable_seqscan = off; -- force index scan
+EXPLAIN (COSTS OFF) SELECT * FROM mvtest_replace WHERE b = 1;
+                       QUERY PLAN                        
+---------------------------------------------------------
+ Index Scan using mvtest_replace_b_idx on mvtest_replace
+   Index Cond: (b = 1)
+(2 rows)
+
+SELECT * FROM mvtest_replace WHERE b = 1;
+ a | b 
+---+---
+ 7 | 1
+(1 row)
+
+RESET enable_seqscan;
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 8 AS a, 1 AS b;
+SET enable_seqscan = off; -- force index scan
+EXPLAIN (COSTS OFF) SELECT * FROM mvtest_replace WHERE b = 1;
+                       QUERY PLAN                        
+---------------------------------------------------------
+ Index Scan using mvtest_replace_b_idx on mvtest_replace
+   Index Cond: (b = 1)
+(2 rows)
+
+SELECT * FROM mvtest_replace WHERE b = 1;
+ a | b 
+---+---
+ 8 | 1
+(1 row)
+
+RESET enable_seqscan;
+-- cannot change column data type
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 9 AS a, 'x' AS b; -- error
+ERROR:  cannot change data type of materialized view column "b" from integer to text
+SELECT * FROM mvtest_replace;
+ a | b 
+---+---
+ 8 | 1
+(1 row)
+
+-- cannot rename column
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 10 AS a, 1 AS b2; -- error
+ERROR:  cannot change name of materialized view column "b" to "b2"
+HINT:  Use ALTER MATERIALIZED VIEW ... RENAME COLUMN ... to change name of materialized view column instead.
+SELECT * FROM mvtest_replace;
+ a | b 
+---+---
+ 8 | 1
+(1 row)
+
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 11 AS a, 1 AS b, 'y' COLLATE "C" AS c;
+SELECT * FROM mvtest_replace;
+ a  | b | c 
+----+---+---
+ 11 | 1 | y
+(1 row)
+
+-- cannot change column collation
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 12 AS a, 1 AS b, 'x' COLLATE "POSIX" AS c; -- error
+ERROR:  cannot change collation of materialized view column "c" from "C" to "POSIX"
+SELECT * FROM mvtest_replace;
+ a  | b | c 
+----+---+---
+ 11 | 1 | y
+(1 row)
+
+-- cannot drop column
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 13 AS a, 1 AS b; -- error
+ERROR:  cannot drop columns from materialized view
+SELECT * FROM mvtest_replace;
+ a  | b | c 
+----+---+---
+ 11 | 1 | y
+(1 row)
+
+-- must target a matview
+CREATE VIEW mvtest_not_mv AS
+  SELECT 1 AS a;
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_not_mv AS
+  SELECT 1 AS a; -- error
+ERROR:  "mvtest_not_mv" is not a materialized view
+DROP VIEW mvtest_not_mv;
+-- cannot use OR REPLACE with IF NOT EXISTS
+CREATE OR REPLACE MATERIALIZED VIEW IF NOT EXISTS mvtest_replace AS
+  SELECT 1 AS a;
+ERROR:  syntax error at or near "NOT"
+LINE 1: CREATE OR REPLACE MATERIALIZED VIEW IF NOT EXISTS mvtest_rep...
+                                               ^
+DROP MATERIALIZED VIEW mvtest_replace;
diff --git a/src/test/regress/sql/matview.sql b/src/test/regress/sql/matview.sql
index 6704eeae2df..d6a4dc4b857 100644
--- a/src/test/regress/sql/matview.sql
+++ b/src/test/regress/sql/matview.sql
@@ -314,3 +314,111 @@ EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF)
   CREATE MATERIALIZED VIEW IF NOT EXISTS matview_ine_tab AS
     SELECT 1 / 0 WITH NO DATA; -- ok
 DROP MATERIALIZED VIEW matview_ine_tab;
+
+--
+-- test CREATE OR REPLACE MATERIALIZED VIEW
+--
+
+-- matview does not already exist
+DROP MATERIALIZED VIEW IF EXISTS mvtest_replace;
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 1 AS a;
+SELECT * FROM mvtest_replace;
+
+-- replace query with data
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 2 AS a;
+SELECT * FROM mvtest_replace;
+
+-- replace query without data
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 3 AS a
+  WITH NO DATA;
+SELECT * FROM mvtest_replace; -- error: not populated
+REFRESH MATERIALIZED VIEW mvtest_replace;
+SELECT * FROM mvtest_replace;
+
+-- add column
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 4 AS a, 1 b;
+SELECT * FROM mvtest_replace;
+
+-- replace table options
+SELECT m.*, c.relname, c.reloptions, s.spcname, a.amname
+  FROM mvtest_replace m
+    CROSS JOIN pg_class c
+    LEFT JOIN pg_tablespace s ON s.oid = c.reltablespace
+    LEFT JOIN pg_am a ON a.oid = c.relam
+  WHERE c.relname = 'mvtest_replace';
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace
+  USING heap2
+  WITH (fillfactor = 50)
+  TABLESPACE regress_tblspace
+  AS SELECT 5 AS a, 1 AS b;
+SELECT m.*, c.relname, c.reloptions, s.spcname, a.amname
+  FROM mvtest_replace m
+    CROSS JOIN pg_class c
+    LEFT JOIN pg_tablespace s ON s.oid = c.reltablespace
+    LEFT JOIN pg_am a ON a.oid = c.relam
+  WHERE c.relname = 'mvtest_replace';
+
+-- can replace matview that has a dependent view
+CREATE VIEW mvtest_replace_v AS
+  SELECT * FROM mvtest_replace;
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 6 AS a, 1 AS b;
+SELECT * FROM mvtest_replace, mvtest_replace_v;
+DROP VIEW mvtest_replace_v;
+
+-- index gets rebuilt when replacing with data
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 7 AS a, 1 AS b;
+CREATE UNIQUE INDEX ON mvtest_replace (b);
+SELECT * FROM mvtest_replace;
+SET enable_seqscan = off; -- force index scan
+EXPLAIN (COSTS OFF) SELECT * FROM mvtest_replace WHERE b = 1;
+SELECT * FROM mvtest_replace WHERE b = 1;
+RESET enable_seqscan;
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 8 AS a, 1 AS b;
+SET enable_seqscan = off; -- force index scan
+EXPLAIN (COSTS OFF) SELECT * FROM mvtest_replace WHERE b = 1;
+SELECT * FROM mvtest_replace WHERE b = 1;
+RESET enable_seqscan;
+
+-- cannot change column data type
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 9 AS a, 'x' AS b; -- error
+SELECT * FROM mvtest_replace;
+
+-- cannot rename column
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 10 AS a, 1 AS b2; -- error
+SELECT * FROM mvtest_replace;
+
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 11 AS a, 1 AS b, 'y' COLLATE "C" AS c;
+SELECT * FROM mvtest_replace;
+
+-- cannot change column collation
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 12 AS a, 1 AS b, 'x' COLLATE "POSIX" AS c; -- error
+SELECT * FROM mvtest_replace;
+
+-- cannot drop column
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 13 AS a, 1 AS b; -- error
+SELECT * FROM mvtest_replace;
+
+-- must target a matview
+CREATE VIEW mvtest_not_mv AS
+  SELECT 1 AS a;
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_not_mv AS
+  SELECT 1 AS a; -- error
+DROP VIEW mvtest_not_mv;
+
+-- cannot use OR REPLACE with IF NOT EXISTS
+CREATE OR REPLACE MATERIALIZED VIEW IF NOT EXISTS mvtest_replace AS
+  SELECT 1 AS a;
+
+DROP MATERIALIZED VIEW mvtest_replace;
-- 
2.48.1

  [text/plain] v6-0002-Deprecate-CREATE-MATERIALIZED-VIEW-IF-NOT-EXISTS.patch (11.0K, ../../bf64dad9-eaaa-4911-b447-13acee89ea80@ewie.name/3-v6-0002-Deprecate-CREATE-MATERIALIZED-VIEW-IF-NOT-EXISTS.patch)
  download | inline diff:
From 232b1439f6cb5d931540901b8e582bd4e6551abf Mon Sep 17 00:00:00 2001
From: Erik Wienhold <ewie@ewie.name>
Date: Tue, 28 May 2024 02:19:53 +0200
Subject: [PATCH v6 2/3] Deprecate CREATE MATERIALIZED VIEW IF NOT EXISTS

---
 src/backend/parser/gram.y                     | 14 ++++++
 .../expected/test_extensions.out              | 45 +++++++++++++++++++
 src/test/regress/expected/matview.out         | 20 +++++++++
 3 files changed, 79 insertions(+)

diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index d2edc460378..8be790eaccf 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -4894,6 +4894,20 @@ CreateMatViewStmt:
 					$8->rel->relpersistence = $2;
 					$8->skipData = !($11);
 					$$ = (Node *) ctas;
+
+					if (ctas->into->rel->schemaname)
+							ereport(WARNING,
+									errmsg("IF NOT EXISTS is deprecated in materialized view creation"),
+									errhint("Use CREATE OR REPLACE MATERIALIZED VIEW %s.%s.",
+											ctas->into->rel->schemaname,
+											ctas->into->rel->relname),
+									parser_errposition(@1));
+					else
+							ereport(WARNING,
+									errmsg("IF NOT EXISTS is deprecated in materialized view creation"),
+									errhint("Use CREATE OR REPLACE MATERIALIZED VIEW %s.",
+											ctas->into->rel->relname),
+									parser_errposition(@1));
 				}
 		| CREATE OR REPLACE OptNoLog MATERIALIZED VIEW create_mv_target AS SelectStmt opt_with_data
 				{
diff --git a/src/test/modules/test_extensions/expected/test_extensions.out b/src/test/modules/test_extensions/expected/test_extensions.out
index d5388a1fecf..9a1e0f76587 100644
--- a/src/test/modules/test_extensions/expected/test_extensions.out
+++ b/src/test/modules/test_extensions/expected/test_extensions.out
@@ -404,6 +404,11 @@ Objects in extension "test_ext_cor"
 CREATE COLLATION ext_cine_coll
   ( LC_COLLATE = "C", LC_CTYPE = "C" );
 CREATE EXTENSION test_ext_cine;  -- fail
+WARNING:  IF NOT EXISTS is deprecated in materialized view creation
+LINE 1: CREATE MATERIALIZED VIEW IF NOT EXISTS ext_cine_mv AS SELECT...
+        ^
+HINT:  Use CREATE OR REPLACE MATERIALIZED VIEW ext_cine_mv.
+QUERY:  CREATE MATERIALIZED VIEW IF NOT EXISTS ext_cine_mv AS SELECT 42 AS f1;
 ERROR:  collation ext_cine_coll is not a member of extension "test_ext_cine"
 DETAIL:  An extension may only use CREATE ... IF NOT EXISTS to skip object creation if the conflicting object is one that it already owns.
 CONTEXT:  SQL statement "CREATE COLLATION IF NOT EXISTS ext_cine_coll
@@ -412,6 +417,11 @@ extension script file "test_ext_cine--1.0.sql", near line 10
 DROP COLLATION ext_cine_coll;
 CREATE MATERIALIZED VIEW ext_cine_mv AS SELECT 11 AS f1;
 CREATE EXTENSION test_ext_cine;  -- fail
+WARNING:  IF NOT EXISTS is deprecated in materialized view creation
+LINE 1: CREATE MATERIALIZED VIEW IF NOT EXISTS ext_cine_mv AS SELECT...
+        ^
+HINT:  Use CREATE OR REPLACE MATERIALIZED VIEW ext_cine_mv.
+QUERY:  CREATE MATERIALIZED VIEW IF NOT EXISTS ext_cine_mv AS SELECT 42 AS f1;
 ERROR:  materialized view ext_cine_mv is not a member of extension "test_ext_cine"
 DETAIL:  An extension may only use CREATE ... IF NOT EXISTS to skip object creation if the conflicting object is one that it already owns.
 CONTEXT:  SQL statement "CREATE MATERIALIZED VIEW IF NOT EXISTS ext_cine_mv AS SELECT 42 AS f1"
@@ -420,6 +430,11 @@ DROP MATERIALIZED VIEW ext_cine_mv;
 CREATE FOREIGN DATA WRAPPER dummy;
 CREATE SERVER ext_cine_srv FOREIGN DATA WRAPPER dummy;
 CREATE EXTENSION test_ext_cine;  -- fail
+WARNING:  IF NOT EXISTS is deprecated in materialized view creation
+LINE 1: CREATE MATERIALIZED VIEW IF NOT EXISTS ext_cine_mv AS SELECT...
+        ^
+HINT:  Use CREATE OR REPLACE MATERIALIZED VIEW ext_cine_mv.
+QUERY:  CREATE MATERIALIZED VIEW IF NOT EXISTS ext_cine_mv AS SELECT 42 AS f1;
 ERROR:  server ext_cine_srv is not a member of extension "test_ext_cine"
 DETAIL:  An extension may only use CREATE ... IF NOT EXISTS to skip object creation if the conflicting object is one that it already owns.
 CONTEXT:  SQL statement "CREATE SERVER IF NOT EXISTS ext_cine_srv FOREIGN DATA WRAPPER ext_cine_fdw"
@@ -427,6 +442,11 @@ extension script file "test_ext_cine--1.0.sql", near line 17
 DROP SERVER ext_cine_srv;
 CREATE SCHEMA ext_cine_schema;
 CREATE EXTENSION test_ext_cine;  -- fail
+WARNING:  IF NOT EXISTS is deprecated in materialized view creation
+LINE 1: CREATE MATERIALIZED VIEW IF NOT EXISTS ext_cine_mv AS SELECT...
+        ^
+HINT:  Use CREATE OR REPLACE MATERIALIZED VIEW ext_cine_mv.
+QUERY:  CREATE MATERIALIZED VIEW IF NOT EXISTS ext_cine_mv AS SELECT 42 AS f1;
 ERROR:  schema ext_cine_schema is not a member of extension "test_ext_cine"
 DETAIL:  An extension may only use CREATE ... IF NOT EXISTS to skip object creation if the conflicting object is one that it already owns.
 CONTEXT:  SQL statement "CREATE SCHEMA IF NOT EXISTS ext_cine_schema"
@@ -434,6 +454,11 @@ extension script file "test_ext_cine--1.0.sql", near line 19
 DROP SCHEMA ext_cine_schema;
 CREATE SEQUENCE ext_cine_seq;
 CREATE EXTENSION test_ext_cine;  -- fail
+WARNING:  IF NOT EXISTS is deprecated in materialized view creation
+LINE 1: CREATE MATERIALIZED VIEW IF NOT EXISTS ext_cine_mv AS SELECT...
+        ^
+HINT:  Use CREATE OR REPLACE MATERIALIZED VIEW ext_cine_mv.
+QUERY:  CREATE MATERIALIZED VIEW IF NOT EXISTS ext_cine_mv AS SELECT 42 AS f1;
 ERROR:  sequence ext_cine_seq is not a member of extension "test_ext_cine"
 DETAIL:  An extension may only use CREATE ... IF NOT EXISTS to skip object creation if the conflicting object is one that it already owns.
 CONTEXT:  SQL statement "CREATE SEQUENCE IF NOT EXISTS ext_cine_seq"
@@ -441,6 +466,11 @@ extension script file "test_ext_cine--1.0.sql", near line 21
 DROP SEQUENCE ext_cine_seq;
 CREATE TABLE ext_cine_tab1 (x int);
 CREATE EXTENSION test_ext_cine;  -- fail
+WARNING:  IF NOT EXISTS is deprecated in materialized view creation
+LINE 1: CREATE MATERIALIZED VIEW IF NOT EXISTS ext_cine_mv AS SELECT...
+        ^
+HINT:  Use CREATE OR REPLACE MATERIALIZED VIEW ext_cine_mv.
+QUERY:  CREATE MATERIALIZED VIEW IF NOT EXISTS ext_cine_mv AS SELECT 42 AS f1;
 ERROR:  table ext_cine_tab1 is not a member of extension "test_ext_cine"
 DETAIL:  An extension may only use CREATE ... IF NOT EXISTS to skip object creation if the conflicting object is one that it already owns.
 CONTEXT:  SQL statement "CREATE TABLE IF NOT EXISTS ext_cine_tab1 (x int)"
@@ -448,12 +478,22 @@ extension script file "test_ext_cine--1.0.sql", near line 23
 DROP TABLE ext_cine_tab1;
 CREATE TABLE ext_cine_tab2 AS SELECT 42 AS y;
 CREATE EXTENSION test_ext_cine;  -- fail
+WARNING:  IF NOT EXISTS is deprecated in materialized view creation
+LINE 1: CREATE MATERIALIZED VIEW IF NOT EXISTS ext_cine_mv AS SELECT...
+        ^
+HINT:  Use CREATE OR REPLACE MATERIALIZED VIEW ext_cine_mv.
+QUERY:  CREATE MATERIALIZED VIEW IF NOT EXISTS ext_cine_mv AS SELECT 42 AS f1;
 ERROR:  table ext_cine_tab2 is not a member of extension "test_ext_cine"
 DETAIL:  An extension may only use CREATE ... IF NOT EXISTS to skip object creation if the conflicting object is one that it already owns.
 CONTEXT:  SQL statement "CREATE TABLE IF NOT EXISTS ext_cine_tab2 AS SELECT 42 AS y"
 extension script file "test_ext_cine--1.0.sql", near line 25
 DROP TABLE ext_cine_tab2;
 CREATE EXTENSION test_ext_cine;
+WARNING:  IF NOT EXISTS is deprecated in materialized view creation
+LINE 1: CREATE MATERIALIZED VIEW IF NOT EXISTS ext_cine_mv AS SELECT...
+        ^
+HINT:  Use CREATE OR REPLACE MATERIALIZED VIEW ext_cine_mv.
+QUERY:  CREATE MATERIALIZED VIEW IF NOT EXISTS ext_cine_mv AS SELECT 42 AS f1;
 \dx+ test_ext_cine
 Objects in extension "test_ext_cine"
         Object description         
@@ -475,6 +515,11 @@ Objects in extension "test_ext_cine"
 (14 rows)
 
 ALTER EXTENSION test_ext_cine UPDATE TO '1.1';
+WARNING:  IF NOT EXISTS is deprecated in materialized view creation
+LINE 1: CREATE MATERIALIZED VIEW IF NOT EXISTS ext_cine_mv AS SELECT...
+        ^
+HINT:  Use CREATE OR REPLACE MATERIALIZED VIEW ext_cine_mv.
+QUERY:  CREATE MATERIALIZED VIEW IF NOT EXISTS ext_cine_mv AS SELECT 42 AS f1;
 \dx+ test_ext_cine
 Objects in extension "test_ext_cine"
         Object description         
diff --git a/src/test/regress/expected/matview.out b/src/test/regress/expected/matview.out
index c0cf8ecbffc..d4b42da1ddf 100644
--- a/src/test/regress/expected/matview.out
+++ b/src/test/regress/expected/matview.out
@@ -565,6 +565,10 @@ CREATE MATERIALIZED VIEW mvtest_mv_foo AS SELECT * FROM mvtest_foo_data;
 CREATE MATERIALIZED VIEW mvtest_mv_foo AS SELECT * FROM mvtest_foo_data;
 ERROR:  relation "mvtest_mv_foo" already exists
 CREATE MATERIALIZED VIEW IF NOT EXISTS mvtest_mv_foo AS SELECT * FROM mvtest_foo_data;
+WARNING:  IF NOT EXISTS is deprecated in materialized view creation
+LINE 1: CREATE MATERIALIZED VIEW IF NOT EXISTS mvtest_mv_foo AS SELE...
+        ^
+HINT:  Use CREATE OR REPLACE MATERIALIZED VIEW mvtest_mv_foo.
 NOTICE:  relation "mvtest_mv_foo" already exists, skipping
 CREATE UNIQUE INDEX ON mvtest_mv_foo (i);
 RESET ROLE;
@@ -662,12 +666,20 @@ CREATE MATERIALIZED VIEW matview_ine_tab AS SELECT 1 / 0; -- error
 ERROR:  relation "matview_ine_tab" already exists
 CREATE MATERIALIZED VIEW IF NOT EXISTS matview_ine_tab AS
   SELECT 1 / 0; -- ok
+WARNING:  IF NOT EXISTS is deprecated in materialized view creation
+LINE 1: CREATE MATERIALIZED VIEW IF NOT EXISTS matview_ine_tab AS
+        ^
+HINT:  Use CREATE OR REPLACE MATERIALIZED VIEW matview_ine_tab.
 NOTICE:  relation "matview_ine_tab" already exists, skipping
 CREATE MATERIALIZED VIEW matview_ine_tab AS
   SELECT 1 / 0 WITH NO DATA; -- error
 ERROR:  relation "matview_ine_tab" already exists
 CREATE MATERIALIZED VIEW IF NOT EXISTS matview_ine_tab AS
   SELECT 1 / 0 WITH NO DATA; -- ok
+WARNING:  IF NOT EXISTS is deprecated in materialized view creation
+LINE 1: CREATE MATERIALIZED VIEW IF NOT EXISTS matview_ine_tab AS
+        ^
+HINT:  Use CREATE OR REPLACE MATERIALIZED VIEW matview_ine_tab.
 NOTICE:  relation "matview_ine_tab" already exists, skipping
 EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF)
   CREATE MATERIALIZED VIEW matview_ine_tab AS
@@ -676,6 +688,10 @@ ERROR:  relation "matview_ine_tab" already exists
 EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF)
   CREATE MATERIALIZED VIEW IF NOT EXISTS matview_ine_tab AS
     SELECT 1 / 0; -- ok
+WARNING:  IF NOT EXISTS is deprecated in materialized view creation
+LINE 2:   CREATE MATERIALIZED VIEW IF NOT EXISTS matview_ine_tab AS
+          ^
+HINT:  Use CREATE OR REPLACE MATERIALIZED VIEW matview_ine_tab.
 NOTICE:  relation "matview_ine_tab" already exists, skipping
  QUERY PLAN 
 ------------
@@ -688,6 +704,10 @@ ERROR:  relation "matview_ine_tab" already exists
 EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF)
   CREATE MATERIALIZED VIEW IF NOT EXISTS matview_ine_tab AS
     SELECT 1 / 0 WITH NO DATA; -- ok
+WARNING:  IF NOT EXISTS is deprecated in materialized view creation
+LINE 2:   CREATE MATERIALIZED VIEW IF NOT EXISTS matview_ine_tab AS
+          ^
+HINT:  Use CREATE OR REPLACE MATERIALIZED VIEW matview_ine_tab.
 NOTICE:  relation "matview_ine_tab" already exists, skipping
  QUERY PLAN 
 ------------
-- 
2.48.1

  [text/plain] v6-0003-Replace-matview-WITH-OLD-DATA.patch (7.7K, ../../bf64dad9-eaaa-4911-b447-13acee89ea80@ewie.name/4-v6-0003-Replace-matview-WITH-OLD-DATA.patch)
  download | inline diff:
From eba3f5904ff97217a07628948fd3c707f5d9e94d Mon Sep 17 00:00:00 2001
From: Erik Wienhold <ewie@ewie.name>
Date: Fri, 26 Jul 2024 23:33:15 +0200
Subject: [PATCH v6 3/3] Replace matview WITH OLD DATA

---
 .../sgml/ref/create_materialized_view.sgml    | 16 +++++++++--
 src/backend/commands/createas.c               | 26 +++++++++++------
 src/backend/parser/gram.y                     | 16 +++++++++++
 src/include/nodes/primnodes.h                 |  1 +
 src/test/regress/expected/matview.out         | 28 +++++++++++++++++++
 src/test/regress/sql/matview.sql              | 15 ++++++++++
 6 files changed, 90 insertions(+), 12 deletions(-)

diff --git a/doc/src/sgml/ref/create_materialized_view.sgml b/doc/src/sgml/ref/create_materialized_view.sgml
index 5e03320eb73..1352e9de409 100644
--- a/doc/src/sgml/ref/create_materialized_view.sgml
+++ b/doc/src/sgml/ref/create_materialized_view.sgml
@@ -27,7 +27,7 @@ CREATE [ OR REPLACE ] MATERIALIZED VIEW [ IF NOT EXISTS ] <replaceable>table_nam
     [ WITH ( <replaceable class="parameter">storage_parameter</replaceable> [= <replaceable class="parameter">value</replaceable>] [, ... ] ) ]
     [ TABLESPACE <replaceable class="parameter">tablespace_name</replaceable> ]
     AS <replaceable>query</replaceable>
-    [ WITH [ NO ] DATA ]
+    [ WITH [ NO | OLD ] DATA ]
 </synopsis>
  </refsynopsisdiv>
 
@@ -37,7 +37,8 @@ CREATE [ OR REPLACE ] MATERIALIZED VIEW [ IF NOT EXISTS ] <replaceable>table_nam
   <para>
    <command>CREATE MATERIALIZED VIEW</command> defines a materialized view of
    a query.  The query is executed and used to populate the view at the time
-   the command is issued (unless <command>WITH NO DATA</command> is used) and may be
+   the command is issued (unless <command>WITH NO DATA</command> or
+   <command>WITH OLD DATA</command> is used) and may be
    refreshed later using <command>REFRESH MATERIALIZED VIEW</command>.
   </para>
 
@@ -162,7 +163,7 @@ CREATE [ OR REPLACE ] MATERIALIZED VIEW [ IF NOT EXISTS ] <replaceable>table_nam
    </varlistentry>
 
    <varlistentry>
-    <term><literal>WITH [ NO ] DATA</literal></term>
+    <term><literal>WITH [ NO | OLD ] DATA</literal></term>
     <listitem>
      <para>
       This clause specifies whether or not the materialized view should be
@@ -170,6 +171,15 @@ CREATE [ OR REPLACE ] MATERIALIZED VIEW [ IF NOT EXISTS ] <replaceable>table_nam
       flagged as unscannable and cannot be queried until <command>REFRESH
       MATERIALIZED VIEW</command> is used.
      </para>
+
+     <para>
+      The form <command>WITH OLD DATA</command> keeps the already stored data
+      when replacing an existing materialized view to keep it populated.  For
+      newly created materialized views, this has the same effect as
+      <command>WITH DATA</command>.  Use this form if you want to use
+      <command>REFRESH MATERIALIZED VIEW CONCURRENTLY</command> as it requires
+      a populated materialized view.
+     </para>
     </listitem>
    </varlistentry>
 
diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c
index 9f37ed7f177..0ed44b246bd 100644
--- a/src/backend/commands/createas.c
+++ b/src/backend/commands/createas.c
@@ -332,18 +332,26 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt,
 		/* An existing materialized view can be replaced. */
 		if (is_matview && into->replace)
 		{
-			RefreshMatViewStmt *refresh;
-
 			/* Change the relation to match the new query and other options. */
-			(void) create_ctas_nodata(query->targetList, into);
+			address = create_ctas_nodata(query->targetList, into);
 
-			/* Refresh the materialized view with a fake statement. */
-			refresh = makeNode(RefreshMatViewStmt);
-			refresh->relation = into->rel;
-			refresh->skipData = into->skipData;
-			refresh->concurrent = false;
+			/*
+			 * Refresh the materialized view with a fake statement unless we
+			 * must keep the old data.
+			 */
+			if (!into->keepData)
+			{
+				RefreshMatViewStmt *refresh;
+
+				refresh = makeNode(RefreshMatViewStmt);
+				refresh->relation = into->rel;
+				refresh->skipData = into->skipData;
+				refresh->concurrent = false;
+
+				address = ExecRefreshMatView(refresh, pstate->p_sourcetext, qc);
+			}
 
-			return ExecRefreshMatView(refresh, pstate->p_sourcetext, qc);
+			return address;
 		}
 
 		return InvalidObjectAddress;
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 8be790eaccf..e405268f413 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -4924,6 +4924,22 @@ CreateMatViewStmt:
 					$7->replace = true;
 					$$ = (Node *) ctas;
 				}
+		| CREATE OR REPLACE OptNoLog MATERIALIZED VIEW create_mv_target AS SelectStmt WITH OLD DATA_P
+				{
+					CreateTableAsStmt *ctas = makeNode(CreateTableAsStmt);
+
+					ctas->query = $9;
+					ctas->into = $7;
+					ctas->objtype = OBJECT_MATVIEW;
+					ctas->is_select_into = false;
+					ctas->if_not_exists = false;
+					/* cram additional flags into the IntoClause */
+					$7->rel->relpersistence = $4;
+					$7->skipData = false;
+					$7->keepData = true;
+					$7->replace = true;
+					$$ = (Node *) ctas;
+				}
 		;
 
 create_mv_target:
diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h
index e800d13a6e0..5290e6c0e8a 100644
--- a/src/include/nodes/primnodes.h
+++ b/src/include/nodes/primnodes.h
@@ -169,6 +169,7 @@ typedef struct IntoClause
 	/* materialized view's SELECT query */
 	struct Query *viewQuery pg_node_attr(query_jumble_ignore);
 	bool		skipData;		/* true for WITH NO DATA */
+	bool		keepData;		/* true for WITH OLD DATA */
 	bool		replace;		/* replace existing matview? */
 } IntoClause;
 
diff --git a/src/test/regress/expected/matview.out b/src/test/regress/expected/matview.out
index d4b42da1ddf..f0f52dd1c6c 100644
--- a/src/test/regress/expected/matview.out
+++ b/src/test/regress/expected/matview.out
@@ -751,6 +751,23 @@ SELECT * FROM mvtest_replace;
  3
 (1 row)
 
+-- replace query but keep old data
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 5 AS a
+  WITH OLD DATA;
+SELECT * FROM mvtest_replace;
+ a 
+---
+ 3
+(1 row)
+
+REFRESH MATERIALIZED VIEW mvtest_replace;
+SELECT * FROM mvtest_replace;
+ a 
+---
+ 5
+(1 row)
+
 -- add column
 CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
   SELECT 4 AS a, 1 b;
@@ -905,3 +922,14 @@ ERROR:  syntax error at or near "NOT"
 LINE 1: CREATE OR REPLACE MATERIALIZED VIEW IF NOT EXISTS mvtest_rep...
                                                ^
 DROP MATERIALIZED VIEW mvtest_replace;
+-- Create new matview WITH OLD DATA.  This populates the new matview as if
+-- WITH DATA had been specified.
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 17 AS a
+  WITH OLD DATA;
+SELECT * FROM mvtest_replace;
+ a  
+----
+ 17
+(1 row)
+
diff --git a/src/test/regress/sql/matview.sql b/src/test/regress/sql/matview.sql
index d6a4dc4b857..91f547e9cb8 100644
--- a/src/test/regress/sql/matview.sql
+++ b/src/test/regress/sql/matview.sql
@@ -338,6 +338,14 @@ SELECT * FROM mvtest_replace; -- error: not populated
 REFRESH MATERIALIZED VIEW mvtest_replace;
 SELECT * FROM mvtest_replace;
 
+-- replace query but keep old data
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 5 AS a
+  WITH OLD DATA;
+SELECT * FROM mvtest_replace;
+REFRESH MATERIALIZED VIEW mvtest_replace;
+SELECT * FROM mvtest_replace;
+
 -- add column
 CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
   SELECT 4 AS a, 1 b;
@@ -422,3 +430,10 @@ CREATE OR REPLACE MATERIALIZED VIEW IF NOT EXISTS mvtest_replace AS
   SELECT 1 AS a;
 
 DROP MATERIALIZED VIEW mvtest_replace;
+
+-- Create new matview WITH OLD DATA.  This populates the new matview as if
+-- WITH DATA had been specified.
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 17 AS a
+  WITH OLD DATA;
+SELECT * FROM mvtest_replace;
-- 
2.48.1

^ permalink  raw  reply  [nested|flat] 29+ messages in thread

* Re: CREATE OR REPLACE MATERIALIZED VIEW
@ 2025-04-05 20:37  Tom Lane <tgl@sss.pgh.pa.us>
  parent: Erik Wienhold <ewie@ewie.name>
  0 siblings, 1 reply; 29+ messages in thread

From: Tom Lane @ 2025-04-05 20:37 UTC (permalink / raw)
  To: Erik Wienhold <ewie@ewie.name>; +Cc: Said Assemlal <sassemlal@neurorx.com>; pgsql-hackers

Erik Wienhold <ewie@ewie.name> writes:
> [ v6 patches ]

A couple of drive-by comments:

* I think the proposal to deprecate IF NOT EXISTS is a nonstarter.
Yeah, I don't like it much, but the standard of proof to remove
features is amazingly high and I don't think it's been reached here.
We're unlikely to remove IF NOT EXISTS for tables, and to the extent
that matviews are like tables it's reasonable for them to have it too.

* On the other hand, the semantics you've implemented for CREATE OR
REPLACE are not right.  The contract for any form of C.O.R. is that
it will either fail, or produce exactly the same object definition
that you would have gotten from plain CREATE with no conflicting
object.  The v6 code is visibly not doing that for properties such
as tablespace --- if the command doesn't mention that, you don't
get the default tablespace, you get whatever the old object had.

* BTW, I'm inclined to think that WITH OLD DATA ought to fail
if the command isn't replacing an existing matview.  It seems
inconsistent to silently reinterpret it as WITH DATA, just as
silently reinterpreting "no tablespace mentioned" as "use the
old tablespace" is inconsistent.  I'm not dead set on that
but it feels wrong.

			regards, tom lane





^ permalink  raw  reply  [nested|flat] 29+ messages in thread

* Re: CREATE OR REPLACE MATERIALIZED VIEW
@ 2025-08-05 01:17  Erik Wienhold <ewie@ewie.name>
  parent: Tom Lane <tgl@sss.pgh.pa.us>
  0 siblings, 2 replies; 29+ messages in thread

From: Erik Wienhold @ 2025-08-05 01:17 UTC (permalink / raw)
  To: Tom Lane <tgl@sss.pgh.pa.us>; +Cc: Said Assemlal <sassemlal@neurorx.com>; pgsql-hackers

Sorry for the late reply but I haven't had the time to dig into this.
Here's v7 fixing the points below.

On 2025-04-05 22:37 +0200, Tom Lane wrote:
> * I think the proposal to deprecate IF NOT EXISTS is a nonstarter.
> Yeah, I don't like it much, but the standard of proof to remove
> features is amazingly high and I don't think it's been reached here.
> We're unlikely to remove IF NOT EXISTS for tables, and to the extent
> that matviews are like tables it's reasonable for them to have it too.

Yeah, I got that gist from the replies upthread and dropped that patch.

> * On the other hand, the semantics you've implemented for CREATE OR
> REPLACE are not right.  The contract for any form of C.O.R. is that
> it will either fail, or produce exactly the same object definition
> that you would have gotten from plain CREATE with no conflicting
> object.  The v6 code is visibly not doing that for properties such
> as tablespace --- if the command doesn't mention that, you don't
> get the default tablespace, you get whatever the old object had.

Thanks a lot.  I added a test case for that and v7-0001 now restores the
default options if none are specified.  Handling the default tablespace
is a bit cumbersome IMO because its name must be passed to
AlterTableInternal.  With v7-0002 I moved that to ATPrepSetTableSpace as
an alternative using the empty string as stand-in for the default
tablespace.  What do you think?

> * BTW, I'm inclined to think that WITH OLD DATA ought to fail
> if the command isn't replacing an existing matview.  It seems
> inconsistent to silently reinterpret it as WITH DATA, just as
> silently reinterpreting "no tablespace mentioned" as "use the
> old tablespace" is inconsistent.  I'm not dead set on that
> but it feels wrong.

Yes that also felt iffy to me.  It just didn't occur to me to simply
raise an error in ExecCreateTableAs.  Done so in v7-0003.

-- 
Erik Wienhold
From 8fff179bf323ca8b4a96b977ab3193db483114cd Mon Sep 17 00:00:00 2001
From: Erik Wienhold <ewie@ewie.name>
Date: Tue, 21 May 2024 18:35:47 +0200
Subject: [PATCH v7 1/3] Add OR REPLACE option to CREATE MATERIALIZED VIEW

---
 .../sgml/ref/create_materialized_view.sgml    |  15 +-
 src/backend/commands/createas.c               | 219 ++++++++++++++----
 src/backend/commands/tablecmds.c              |   8 +-
 src/backend/commands/view.c                   | 106 ++++++---
 src/backend/parser/gram.y                     |  15 ++
 src/bin/psql/tab-complete.in.c                |  26 ++-
 src/include/commands/view.h                   |   3 +
 src/include/nodes/parsenodes.h                |   2 +-
 src/include/nodes/primnodes.h                 |   1 +
 src/test/regress/expected/matview.out         | 205 ++++++++++++++++
 src/test/regress/sql/matview.sql              | 117 ++++++++++
 11 files changed, 624 insertions(+), 93 deletions(-)

diff --git a/doc/src/sgml/ref/create_materialized_view.sgml b/doc/src/sgml/ref/create_materialized_view.sgml
index 62d897931c3..5e03320eb73 100644
--- a/doc/src/sgml/ref/create_materialized_view.sgml
+++ b/doc/src/sgml/ref/create_materialized_view.sgml
@@ -21,7 +21,7 @@ PostgreSQL documentation
 
  <refsynopsisdiv>
 <synopsis>
-CREATE MATERIALIZED VIEW [ IF NOT EXISTS ] <replaceable>table_name</replaceable>
+CREATE [ OR REPLACE ] MATERIALIZED VIEW [ IF NOT EXISTS ] <replaceable>table_name</replaceable>
     [ (<replaceable>column_name</replaceable> [, ...] ) ]
     [ USING <replaceable class="parameter">method</replaceable> ]
     [ WITH ( <replaceable class="parameter">storage_parameter</replaceable> [= <replaceable class="parameter">value</replaceable>] [, ... ] ) ]
@@ -60,6 +60,17 @@ CREATE MATERIALIZED VIEW [ IF NOT EXISTS ] <replaceable>table_name</replaceable>
   <title>Parameters</title>
 
   <variablelist>
+   <varlistentry>
+    <term><literal>OR REPLACE</literal></term>
+    <listitem>
+     <para>
+      Replaces a materialized view if it already exists.
+      Specifying <literal>OR REPLACE</literal> together with
+      <literal>IF NOT EXISTS</literal> is an error.
+     </para>
+    </listitem>
+   </varlistentry>
+
    <varlistentry>
     <term><literal>IF NOT EXISTS</literal></term>
     <listitem>
@@ -67,7 +78,7 @@ CREATE MATERIALIZED VIEW [ IF NOT EXISTS ] <replaceable>table_name</replaceable>
       Do not throw an error if a materialized view with the same name already
       exists. A notice is issued in this case.  Note that there is no guarantee
       that the existing materialized view is anything like the one that would
-      have been created.
+      have been created, unless you use <literal>OR REPLACE</literal> instead.
      </para>
     </listitem>
    </varlistentry>
diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c
index dfd2ab8e862..1620273f965 100644
--- a/src/backend/commands/createas.c
+++ b/src/backend/commands/createas.c
@@ -24,6 +24,7 @@
  */
 #include "postgres.h"
 
+#include "miscadmin.h"
 #include "access/heapam.h"
 #include "access/reloptions.h"
 #include "access/tableam.h"
@@ -34,6 +35,7 @@
 #include "commands/matview.h"
 #include "commands/prepare.h"
 #include "commands/tablecmds.h"
+#include "commands/tablespace.h"
 #include "commands/view.h"
 #include "executor/execdesc.h"
 #include "executor/executor.h"
@@ -81,55 +83,161 @@ static void intorel_destroy(DestReceiver *self);
 static ObjectAddress
 create_ctas_internal(List *attrList, IntoClause *into)
 {
-	CreateStmt *create = makeNode(CreateStmt);
-	bool		is_matview;
+	bool		is_matview,
+				replace = false;
 	char		relkind;
-	Datum		toast_options;
-	const char *const validnsps[] = HEAP_RELOPT_NAMESPACES;
+	Oid			matviewOid = InvalidOid;
 	ObjectAddress intoRelationAddr;
 
 	/* This code supports both CREATE TABLE AS and CREATE MATERIALIZED VIEW */
 	is_matview = (into->viewQuery != NULL);
 	relkind = is_matview ? RELKIND_MATVIEW : RELKIND_RELATION;
 
-	/*
-	 * Create the target relation by faking up a CREATE TABLE parsetree and
-	 * passing it to DefineRelation.
-	 */
-	create->relation = into->rel;
-	create->tableElts = attrList;
-	create->inhRelations = NIL;
-	create->ofTypename = NULL;
-	create->constraints = NIL;
-	create->options = into->options;
-	create->oncommit = into->onCommit;
-	create->tablespacename = into->tableSpaceName;
-	create->if_not_exists = false;
-	create->accessMethod = into->accessMethod;
+	/* Check if an existing materialized view needs to be replaced. */
+	if (is_matview)
+	{
+		LOCKMODE	lockmode;
 
-	/*
-	 * Create the relation.  (This will error out if there's an existing view,
-	 * so we don't need more code to complain if "replace" is false.)
-	 */
-	intoRelationAddr = DefineRelation(create, relkind, InvalidOid, NULL, NULL);
+		lockmode = into->replace ? AccessExclusiveLock : NoLock;
+		(void) RangeVarGetAndCheckCreationNamespace(into->rel, lockmode,
+													&matviewOid);
+		replace = OidIsValid(matviewOid) && into->replace;
+	}
 
-	/*
-	 * If necessary, create a TOAST table for the target table.  Note that
-	 * NewRelationCreateToastTable ends with CommandCounterIncrement(), so
-	 * that the TOAST table will be visible for insertion.
-	 */
-	CommandCounterIncrement();
+	if (is_matview && replace)
+	{
+		Relation	rel;
+		List	   *atcmds = NIL;
+		AlterTableCmd *atcmd;
+		TupleDesc	descriptor;
+
+		rel = relation_open(matviewOid, NoLock);
+
+		if (rel->rd_rel->relkind != RELKIND_MATVIEW)
+			ereport(ERROR,
+					errcode(ERRCODE_WRONG_OBJECT_TYPE),
+					errmsg("\"%s\" is not a materialized view",
+						   RelationGetRelationName(rel)));
+
+		CheckTableNotInUse(rel, "CREATE OR REPLACE MATERIALIZED VIEW");
+
+		descriptor = BuildDescForRelation(attrList);
+		checkViewColumns(descriptor, rel->rd_att, true);
+
+		/* add new attributes */
+		if (list_length(attrList) > rel->rd_att->natts)
+		{
+			ListCell   *c;
+			int			skip = rel->rd_att->natts;
+
+			foreach(c, attrList)
+			{
+				if (skip > 0)
+				{
+					skip--;
+					continue;
+				}
+				atcmd = makeNode(AlterTableCmd);
+				atcmd->subtype = AT_AddColumnToView;
+				atcmd->def = (Node *) lfirst(c);
+				atcmds = lappend(atcmds, atcmd);
+			}
+		}
+
+		/*
+		 * The following alters access method, tablespace, and storage options.
+		 * When replacing an existing matview we need to alter the relation
+		 * such that the defaults apply as if they have not been specified at
+		 * all by the CREATE statement.
+		 */
+
+		/* access method */
+		atcmd = makeNode(AlterTableCmd);
+		atcmd->subtype = AT_SetAccessMethod;
+		atcmd->name = into->accessMethod ? into->accessMethod : default_table_access_method;
+		atcmds = lappend(atcmds, atcmd);
+
+		/* tablespace */
+		atcmd = makeNode(AlterTableCmd);
+		atcmd->subtype = AT_SetTableSpace;
+		if (into->tableSpaceName != NULL)
+			atcmd->name = into->tableSpaceName;
+		else
+		{
+			Oid spcid;
+
+			/*
+			 * Resolve the name of the default or database tablespace because
+			 * we need to specify the tablespace by name.
+			 *
+			 * TODO: Move that to ATPrepSetTableSpace? Must allow AlterTableCmd.name to be NULL then.
+			 */
+			spcid = GetDefaultTablespace(RELPERSISTENCE_PERMANENT, false);
+			if (!OidIsValid(spcid))
+				spcid = MyDatabaseTableSpace;
+			atcmd->name = get_tablespace_name(spcid);
+		}
+		atcmds = lappend(atcmds, atcmd);
+
+		/* storage options */
+		atcmd = makeNode(AlterTableCmd);
+		atcmd->subtype = AT_ReplaceRelOptions;
+		atcmd->def = (Node *) into->options;
+		atcmds = lappend(atcmds, atcmd);
+
+		AlterTableInternal(matviewOid, atcmds, true);
+		CommandCounterIncrement();
+
+		relation_close(rel, NoLock);
+		ObjectAddressSet(intoRelationAddr, RelationRelationId, matviewOid);
+	}
+	else
+	{
+		CreateStmt *create = makeNode(CreateStmt);
+		Datum		toast_options;
+		const static char *validnsps[] = HEAP_RELOPT_NAMESPACES;
+
+		/*
+		 * Create the target relation by faking up a CREATE TABLE parsetree
+		 * and passing it to DefineRelation.
+		 */
+		create->relation = into->rel;
+		create->tableElts = attrList;
+		create->inhRelations = NIL;
+		create->ofTypename = NULL;
+		create->constraints = NIL;
+		create->options = into->options;
+		create->oncommit = into->onCommit;
+		create->tablespacename = into->tableSpaceName;
+		create->if_not_exists = false;
+		create->accessMethod = into->accessMethod;
+
+		/*
+		 * Create the relation.  (This will error out if there's an existing
+		 * view, so we don't need more code to complain if "replace" is
+		 * false.)
+		 */
+		intoRelationAddr = DefineRelation(create, relkind, InvalidOid, NULL,
+										  NULL);
 
-	/* parse and validate reloptions for the toast table */
-	toast_options = transformRelOptions((Datum) 0,
-										create->options,
-										"toast",
-										validnsps,
-										true, false);
+		/*
+		 * If necessary, create a TOAST table for the target table.  Note that
+		 * NewRelationCreateToastTable ends with CommandCounterIncrement(), so
+		 * that the TOAST table will be visible for insertion.
+		 */
+		CommandCounterIncrement();
+
+		/* parse and validate reloptions for the toast table */
+		toast_options = transformRelOptions((Datum) 0,
+											create->options,
+											"toast",
+											validnsps,
+											true, false);
 
-	(void) heap_reloptions(RELKIND_TOASTVALUE, toast_options, true);
+		(void) heap_reloptions(RELKIND_TOASTVALUE, toast_options, true);
 
-	NewRelationCreateToastTable(intoRelationAddr.objectId, toast_options);
+		NewRelationCreateToastTable(intoRelationAddr.objectId, toast_options);
+	}
 
 	/* Create the "view" part of a materialized view. */
 	if (is_matview)
@@ -137,7 +245,7 @@ create_ctas_internal(List *attrList, IntoClause *into)
 		/* StoreViewQuery scribbles on tree, so make a copy */
 		Query	   *query = copyObject(into->viewQuery);
 
-		StoreViewQuery(intoRelationAddr.objectId, query, false);
+		StoreViewQuery(intoRelationAddr.objectId, query, replace);
 		CommandCounterIncrement();
 	}
 
@@ -234,7 +342,26 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt,
 
 	/* Check if the relation exists or not */
 	if (CreateTableAsRelExists(stmt))
+	{
+		/* An existing materialized view can be replaced. */
+		if (is_matview && into->replace)
+		{
+			RefreshMatViewStmt *refresh;
+
+			/* Change the relation to match the new query and other options. */
+			(void) create_ctas_nodata(query->targetList, into);
+
+			/* Refresh the materialized view with a fake statement. */
+			refresh = makeNode(RefreshMatViewStmt);
+			refresh->relation = into->rel;
+			refresh->skipData = into->skipData;
+			refresh->concurrent = false;
+
+			return ExecRefreshMatView(refresh, pstate->p_sourcetext, qc);
+		}
+
 		return InvalidObjectAddress;
+	}
 
 	/*
 	 * Create the tuple receiver object and insert info it will need
@@ -402,14 +529,15 @@ CreateTableAsRelExists(CreateTableAsStmt *ctas)
 	oldrelid = get_relname_relid(into->rel->relname, nspid);
 	if (OidIsValid(oldrelid))
 	{
-		if (!ctas->if_not_exists)
+		if (!ctas->if_not_exists && !into->replace)
 			ereport(ERROR,
 					(errcode(ERRCODE_DUPLICATE_TABLE),
 					 errmsg("relation \"%s\" already exists",
 							into->rel->relname)));
 
 		/*
-		 * The relation exists and IF NOT EXISTS has been specified.
+		 * The relation exists and IF NOT EXISTS or OR REPLACE has been
+		 * specified.
 		 *
 		 * If we are in an extension script, insist that the pre-existing
 		 * object be a member of the extension, to avoid security risks.
@@ -417,11 +545,12 @@ CreateTableAsRelExists(CreateTableAsStmt *ctas)
 		ObjectAddressSet(address, RelationRelationId, oldrelid);
 		checkMembershipInCurrentExtension(&address);
 
-		/* OK to skip */
-		ereport(NOTICE,
-				(errcode(ERRCODE_DUPLICATE_TABLE),
-				 errmsg("relation \"%s\" already exists, skipping",
-						into->rel->relname)));
+		if (ctas->if_not_exists)
+			/* OK to skip */
+			ereport(NOTICE,
+					(errcode(ERRCODE_DUPLICATE_TABLE),
+					 errmsg("relation \"%s\" already exists, skipping",
+							into->rel->relname)));
 		return true;
 	}
 
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index cb811520c29..44dcd2c5b0d 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -4645,7 +4645,7 @@ AlterTableGetLockLevel(List *cmds)
 				 * Subcommands that may be visible to concurrent SELECTs
 				 */
 			case AT_DropColumn: /* change visible to SELECT */
-			case AT_AddColumnToView:	/* CREATE VIEW */
+			case AT_AddColumnToView:	/* via CREATE OR REPLACE [MATERIALIZED] VIEW */
 			case AT_DropOids:	/* used to equiv to DropColumn */
 			case AT_EnableAlwaysRule:	/* may change SELECT rules */
 			case AT_EnableReplicaRule:	/* may change SELECT rules */
@@ -4940,8 +4940,8 @@ ATPrepCmd(List **wqueue, Relation rel, AlterTableCmd *cmd,
 			/* Recursion occurs during execution phase */
 			pass = AT_PASS_ADD_COL;
 			break;
-		case AT_AddColumnToView:	/* add column via CREATE OR REPLACE VIEW */
-			ATSimplePermissions(cmd->subtype, rel, ATT_VIEW);
+		case AT_AddColumnToView:	/* via CREATE OR REPLACE [MATERIALIZED] VIEW */
+			ATSimplePermissions(cmd->subtype, rel, ATT_VIEW | ATT_MATVIEW);
 			ATPrepAddColumn(wqueue, rel, recurse, recursing, true, cmd,
 							lockmode, context);
 			/* Recursion occurs during execution phase */
@@ -5372,7 +5372,7 @@ ATExecCmd(List **wqueue, AlteredTableInfo *tab,
 	switch (cmd->subtype)
 	{
 		case AT_AddColumn:		/* ADD COLUMN */
-		case AT_AddColumnToView:	/* add column via CREATE OR REPLACE VIEW */
+		case AT_AddColumnToView:	/* via CREATE OR REPLACE [MATERIALIZED] VIEW */
 			address = ATExecAddColumn(wqueue, tab, rel, &cmd,
 									  cmd->recurse, false,
 									  lockmode, cur_pass, context);
diff --git a/src/backend/commands/view.c b/src/backend/commands/view.c
index 6f0301555e0..53d4cacc4c1 100644
--- a/src/backend/commands/view.c
+++ b/src/backend/commands/view.c
@@ -30,8 +30,6 @@
 #include "utils/lsyscache.h"
 #include "utils/rel.h"
 
-static void checkViewColumns(TupleDesc newdesc, TupleDesc olddesc);
-
 /*---------------------------------------------------------------------
  * DefineVirtualRelation
  *
@@ -129,7 +127,7 @@ DefineVirtualRelation(RangeVar *relation, List *tlist, bool replace,
 		 * column list.
 		 */
 		descriptor = BuildDescForRelation(attrList);
-		checkViewColumns(descriptor, rel->rd_att);
+		checkViewColumns(descriptor, rel->rd_att, false);
 
 		/*
 		 * If new attributes have been added, we must add pg_attribute entries
@@ -263,15 +261,22 @@ DefineVirtualRelation(RangeVar *relation, List *tlist, bool replace,
  * added to generate specific complaints.  Also, we allow the new view to have
  * more columns than the old.
  */
-static void
-checkViewColumns(TupleDesc newdesc, TupleDesc olddesc)
+void
+checkViewColumns(TupleDesc newdesc, TupleDesc olddesc, bool is_matview)
 {
 	int			i;
 
 	if (newdesc->natts < olddesc->natts)
-		ereport(ERROR,
-				(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
-				 errmsg("cannot drop columns from view")));
+	{
+		if (is_matview)
+			ereport(ERROR,
+					errcode(ERRCODE_INVALID_TABLE_DEFINITION),
+					errmsg("cannot drop columns from materialized view"));
+		else
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
+					 errmsg("cannot drop columns from view")));
+	}
 
 	for (i = 0; i < olddesc->natts; i++)
 	{
@@ -280,17 +285,34 @@ checkViewColumns(TupleDesc newdesc, TupleDesc olddesc)
 
 		/* XXX msg not right, but we don't support DROP COL on view anyway */
 		if (newattr->attisdropped != oldattr->attisdropped)
-			ereport(ERROR,
-					(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
-					 errmsg("cannot drop columns from view")));
+		{
+			if (is_matview)
+				ereport(ERROR,
+						errcode(ERRCODE_INVALID_TABLE_DEFINITION),
+						errmsg("cannot drop columns from materialized view"));
+			else
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
+						 errmsg("cannot drop columns from view")));
+		}
 
 		if (strcmp(NameStr(newattr->attname), NameStr(oldattr->attname)) != 0)
-			ereport(ERROR,
-					(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
-					 errmsg("cannot change name of view column \"%s\" to \"%s\"",
-							NameStr(oldattr->attname),
-							NameStr(newattr->attname)),
-					 errhint("Use ALTER VIEW ... RENAME COLUMN ... to change name of view column instead.")));
+		{
+			if (is_matview)
+				ereport(ERROR,
+						errcode(ERRCODE_INVALID_TABLE_DEFINITION),
+						errmsg("cannot change name of materialized view column \"%s\" to \"%s\"",
+							   NameStr(oldattr->attname),
+							   NameStr(newattr->attname)),
+						errhint("Use ALTER MATERIALIZED VIEW ... RENAME COLUMN ... to change name of materialized view column instead."));
+			else
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
+						 errmsg("cannot change name of view column \"%s\" to \"%s\"",
+								NameStr(oldattr->attname),
+								NameStr(newattr->attname)),
+						 errhint("Use ALTER VIEW ... RENAME COLUMN ... to change name of view column instead.")));
+		}
 
 		/*
 		 * We cannot allow type, typmod, or collation to change, since these
@@ -299,26 +321,48 @@ checkViewColumns(TupleDesc newdesc, TupleDesc olddesc)
 		 */
 		if (newattr->atttypid != oldattr->atttypid ||
 			newattr->atttypmod != oldattr->atttypmod)
-			ereport(ERROR,
-					(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
-					 errmsg("cannot change data type of view column \"%s\" from %s to %s",
-							NameStr(oldattr->attname),
-							format_type_with_typemod(oldattr->atttypid,
-													 oldattr->atttypmod),
-							format_type_with_typemod(newattr->atttypid,
-													 newattr->atttypmod))));
+		{
+			if (is_matview)
+				ereport(ERROR,
+						errcode(ERRCODE_INVALID_TABLE_DEFINITION),
+						errmsg("cannot change data type of materialized view column \"%s\" from %s to %s",
+							   NameStr(oldattr->attname),
+							   format_type_with_typemod(oldattr->atttypid,
+														oldattr->atttypmod),
+							   format_type_with_typemod(newattr->atttypid,
+														newattr->atttypmod)));
+			else
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
+						 errmsg("cannot change data type of view column \"%s\" from %s to %s",
+								NameStr(oldattr->attname),
+								format_type_with_typemod(oldattr->atttypid,
+														 oldattr->atttypmod),
+								format_type_with_typemod(newattr->atttypid,
+														 newattr->atttypmod))));
+		}
 
 		/*
 		 * At this point, attcollations should be both valid or both invalid,
 		 * so applying get_collation_name unconditionally should be fine.
 		 */
 		if (newattr->attcollation != oldattr->attcollation)
-			ereport(ERROR,
-					(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
-					 errmsg("cannot change collation of view column \"%s\" from \"%s\" to \"%s\"",
-							NameStr(oldattr->attname),
-							get_collation_name(oldattr->attcollation),
-							get_collation_name(newattr->attcollation))));
+		{
+			if (is_matview)
+				ereport(ERROR,
+						errcode(ERRCODE_INVALID_TABLE_DEFINITION),
+						errmsg("cannot change collation of materialized view column \"%s\" from \"%s\" to \"%s\"",
+							   NameStr(oldattr->attname),
+							   get_collation_name(oldattr->attcollation),
+							   get_collation_name(newattr->attcollation)));
+			else
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
+						 errmsg("cannot change collation of view column \"%s\" from \"%s\" to \"%s\"",
+								NameStr(oldattr->attname),
+								get_collation_name(oldattr->attcollation),
+								get_collation_name(newattr->attcollation))));
+		}
 	}
 
 	/*
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index db43034b9db..7aaf0e37ad8 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -4929,6 +4929,21 @@ CreateMatViewStmt:
 					$8->skipData = !($11);
 					$$ = (Node *) ctas;
 				}
+		| CREATE OR REPLACE OptNoLog MATERIALIZED VIEW create_mv_target AS SelectStmt opt_with_data
+				{
+					CreateTableAsStmt *ctas = makeNode(CreateTableAsStmt);
+
+					ctas->query = $9;
+					ctas->into = $7;
+					ctas->objtype = OBJECT_MATVIEW;
+					ctas->is_select_into = false;
+					ctas->if_not_exists = false;
+					/* cram additional flags into the IntoClause */
+					$7->rel->relpersistence = $4;
+					$7->skipData = !($10);
+					$7->replace = true;
+					$$ = (Node *) ctas;
+				}
 		;
 
 create_mv_target:
diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c
index 1f2ca946fc5..f49f0b60c95 100644
--- a/src/bin/psql/tab-complete.in.c
+++ b/src/bin/psql/tab-complete.in.c
@@ -2187,7 +2187,7 @@ match_previous_words(int pattern_id,
 	/* complete with something you can create or replace */
 	else if (TailMatches("CREATE", "OR", "REPLACE"))
 		COMPLETE_WITH("FUNCTION", "PROCEDURE", "LANGUAGE", "RULE", "VIEW",
-					  "AGGREGATE", "TRANSFORM", "TRIGGER");
+					  "AGGREGATE", "TRANSFORM", "TRIGGER", "MATERIALIZED VIEW");
 
 /* DROP, but not DROP embedded in other commands */
 	/* complete with something you can drop */
@@ -4072,28 +4072,34 @@ match_previous_words(int pattern_id,
 		COMPLETE_WITH("SELECT");
 
 /* CREATE MATERIALIZED VIEW */
-	else if (Matches("CREATE", "MATERIALIZED"))
+	else if (Matches("CREATE", "MATERIALIZED") ||
+			 Matches("CREATE", "OR", "REPLACE", "MATERIALIZED"))
 		COMPLETE_WITH("VIEW");
-	/* Complete CREATE MATERIALIZED VIEW <name> with AS or USING */
-	else if (Matches("CREATE", "MATERIALIZED", "VIEW", MatchAny))
+	/* Complete CREATE [ OR REPLACE ] MATERIALIZED VIEW <name> with AS or USING */
+	else if (Matches("CREATE", "MATERIALIZED", "VIEW", MatchAny) ||
+			 Matches("CREATE", "OR", "REPLACE", "MATERIALIZED", "VIEW", MatchAny))
 		COMPLETE_WITH("AS", "USING");
 
 	/*
-	 * Complete CREATE MATERIALIZED VIEW <name> USING with list of access
+	 * Complete CREATE [ OR REPLACE ] MATERIALIZED VIEW <name> USING with list of access
 	 * methods
 	 */
-	else if (Matches("CREATE", "MATERIALIZED", "VIEW", MatchAny, "USING"))
+	else if (Matches("CREATE", "MATERIALIZED", "VIEW", MatchAny, "USING") ||
+			 Matches("CREATE", "OR", "REPLACE", "MATERIALIZED", "VIEW", MatchAny, "USING"))
 		COMPLETE_WITH_QUERY(Query_for_list_of_table_access_methods);
-	/* Complete CREATE MATERIALIZED VIEW <name> USING <access method> with AS */
-	else if (Matches("CREATE", "MATERIALIZED", "VIEW", MatchAny, "USING", MatchAny))
+	/* Complete CREATE [ OR REPLACE ] MATERIALIZED VIEW <name> USING <access method> with AS */
+	else if (Matches("CREATE", "MATERIALIZED", "VIEW", MatchAny, "USING", MatchAny) ||
+			 Matches("CREATE", "OR", "REPLACE", "MATERIALIZED", "VIEW", MatchAny, "USING", MatchAny))
 		COMPLETE_WITH("AS");
 
 	/*
-	 * Complete CREATE MATERIALIZED VIEW <name> [USING <access method> ] AS
+	 * Complete CREATE [ OR REPLACE ] MATERIALIZED VIEW <name> [USING <access method> ] AS
 	 * with "SELECT"
 	 */
 	else if (Matches("CREATE", "MATERIALIZED", "VIEW", MatchAny, "AS") ||
-			 Matches("CREATE", "MATERIALIZED", "VIEW", MatchAny, "USING", MatchAny, "AS"))
+			 Matches("CREATE", "OR", "REPLACE", "MATERIALIZED", "VIEW", MatchAny, "AS") ||
+			 Matches("CREATE", "MATERIALIZED", "VIEW", MatchAny, "USING", MatchAny, "AS") ||
+			 Matches("CREATE", "OR", "REPLACE", "MATERIALIZED", "VIEW", MatchAny, "USING", MatchAny, "AS"))
 		COMPLETE_WITH("SELECT");
 
 /* CREATE EVENT TRIGGER */
diff --git a/src/include/commands/view.h b/src/include/commands/view.h
index c41f51b161c..95290f0a1c4 100644
--- a/src/include/commands/view.h
+++ b/src/include/commands/view.h
@@ -22,4 +22,7 @@ extern ObjectAddress DefineView(ViewStmt *stmt, const char *queryString,
 
 extern void StoreViewQuery(Oid viewOid, Query *viewParse, bool replace);
 
+extern void checkViewColumns(TupleDesc newdesc, TupleDesc olddesc,
+							 bool is_matview);
+
 #endif							/* VIEW_H */
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 86a236bd58b..74922f8ae89 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -2412,7 +2412,7 @@ typedef struct AlterTableStmt
 typedef enum AlterTableType
 {
 	AT_AddColumn,				/* add column */
-	AT_AddColumnToView,			/* implicitly via CREATE OR REPLACE VIEW */
+	AT_AddColumnToView,			/* implicitly via CREATE OR REPLACE [MATERIALIZED] VIEW */
 	AT_ColumnDefault,			/* alter column default */
 	AT_CookedColumnDefault,		/* add a pre-cooked column default */
 	AT_DropNotNull,				/* alter column drop not null */
diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h
index 6dfca3cb35b..3a0c3b06c81 100644
--- a/src/include/nodes/primnodes.h
+++ b/src/include/nodes/primnodes.h
@@ -169,6 +169,7 @@ typedef struct IntoClause
 	/* materialized view's SELECT query */
 	struct Query *viewQuery pg_node_attr(query_jumble_ignore);
 	bool		skipData;		/* true for WITH NO DATA */
+	bool		replace;		/* replace existing matview? */
 } IntoClause;
 
 
diff --git a/src/test/regress/expected/matview.out b/src/test/regress/expected/matview.out
index c56c9fa3a25..56104b7ee8b 100644
--- a/src/test/regress/expected/matview.out
+++ b/src/test/regress/expected/matview.out
@@ -694,3 +694,208 @@ NOTICE:  relation "matview_ine_tab" already exists, skipping
 (0 rows)
 
 DROP MATERIALIZED VIEW matview_ine_tab;
+--
+-- test CREATE OR REPLACE MATERIALIZED VIEW
+--
+-- matview does not already exist
+DROP MATERIALIZED VIEW IF EXISTS mvtest_replace;
+NOTICE:  materialized view "mvtest_replace" does not exist, skipping
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 1 AS a;
+SELECT * FROM mvtest_replace;
+ a 
+---
+ 1
+(1 row)
+
+-- replace query with data
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 2 AS a;
+SELECT * FROM mvtest_replace;
+ a 
+---
+ 2
+(1 row)
+
+-- replace query without data
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 3 AS a
+  WITH NO DATA;
+SELECT * FROM mvtest_replace; -- error: not populated
+ERROR:  materialized view "mvtest_replace" has not been populated
+HINT:  Use the REFRESH MATERIALIZED VIEW command.
+REFRESH MATERIALIZED VIEW mvtest_replace;
+SELECT * FROM mvtest_replace;
+ a 
+---
+ 3
+(1 row)
+
+-- add column
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 4 AS a, 1 b;
+SELECT * FROM mvtest_replace;
+ a | b 
+---+---
+ 4 | 1
+(1 row)
+
+-- replace table options
+SELECT m.*, c.relname, c.reloptions, s.spcname, a.amname
+  FROM mvtest_replace m
+    CROSS JOIN pg_class c
+    LEFT JOIN pg_tablespace s ON s.oid = c.reltablespace
+    LEFT JOIN pg_am a ON a.oid = c.relam
+  WHERE c.relname = 'mvtest_replace';
+ a | b |    relname     | reloptions | spcname | amname 
+---+---+----------------+------------+---------+--------
+ 4 | 1 | mvtest_replace |            |         | heap
+(1 row)
+
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace
+  USING heap2
+  WITH (fillfactor = 50)
+  TABLESPACE regress_tblspace
+  AS SELECT 5 AS a, 1 AS b;
+SELECT m.*, c.relname, c.reloptions, s.spcname, a.amname
+  FROM mvtest_replace m
+    CROSS JOIN pg_class c
+    LEFT JOIN pg_tablespace s ON s.oid = c.reltablespace
+    LEFT JOIN pg_am a ON a.oid = c.relam
+  WHERE c.relname = 'mvtest_replace';
+ a | b |    relname     |   reloptions    |     spcname      | amname 
+---+---+----------------+-----------------+------------------+--------
+ 5 | 1 | mvtest_replace | {fillfactor=50} | regress_tblspace | heap2
+(1 row)
+
+-- restore default options
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace
+  AS SELECT 5 AS a, 1 AS b;
+SELECT m.*, c.relname, c.reloptions, s.spcname, a.amname
+  FROM mvtest_replace m
+    CROSS JOIN pg_class c
+    LEFT JOIN pg_tablespace s ON s.oid = c.reltablespace
+    LEFT JOIN pg_am a ON a.oid = c.relam
+  WHERE c.relname = 'mvtest_replace';
+ a | b |    relname     | reloptions | spcname | amname 
+---+---+----------------+------------+---------+--------
+ 5 | 1 | mvtest_replace |            |         | heap
+(1 row)
+
+-- can replace matview that has a dependent view
+CREATE VIEW mvtest_replace_v AS
+  SELECT * FROM mvtest_replace;
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 6 AS a, 1 AS b;
+SELECT * FROM mvtest_replace, mvtest_replace_v;
+ a | b | a | b 
+---+---+---+---
+ 6 | 1 | 6 | 1
+(1 row)
+
+DROP VIEW mvtest_replace_v;
+-- index gets rebuilt when replacing with data
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 7 AS a, 1 AS b;
+CREATE UNIQUE INDEX ON mvtest_replace (b);
+SELECT * FROM mvtest_replace;
+ a | b 
+---+---
+ 7 | 1
+(1 row)
+
+SET enable_seqscan = off; -- force index scan
+EXPLAIN (COSTS OFF) SELECT * FROM mvtest_replace WHERE b = 1;
+                       QUERY PLAN                        
+---------------------------------------------------------
+ Index Scan using mvtest_replace_b_idx on mvtest_replace
+   Index Cond: (b = 1)
+(2 rows)
+
+SELECT * FROM mvtest_replace WHERE b = 1;
+ a | b 
+---+---
+ 7 | 1
+(1 row)
+
+RESET enable_seqscan;
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 8 AS a, 1 AS b;
+SET enable_seqscan = off; -- force index scan
+EXPLAIN (COSTS OFF) SELECT * FROM mvtest_replace WHERE b = 1;
+                       QUERY PLAN                        
+---------------------------------------------------------
+ Index Scan using mvtest_replace_b_idx on mvtest_replace
+   Index Cond: (b = 1)
+(2 rows)
+
+SELECT * FROM mvtest_replace WHERE b = 1;
+ a | b 
+---+---
+ 8 | 1
+(1 row)
+
+RESET enable_seqscan;
+-- cannot change column data type
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 9 AS a, 'x' AS b; -- error
+ERROR:  cannot change data type of materialized view column "b" from integer to text
+SELECT * FROM mvtest_replace;
+ a | b 
+---+---
+ 8 | 1
+(1 row)
+
+-- cannot rename column
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 10 AS a, 1 AS b2; -- error
+ERROR:  cannot change name of materialized view column "b" to "b2"
+HINT:  Use ALTER MATERIALIZED VIEW ... RENAME COLUMN ... to change name of materialized view column instead.
+SELECT * FROM mvtest_replace;
+ a | b 
+---+---
+ 8 | 1
+(1 row)
+
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 11 AS a, 1 AS b, 'y' COLLATE "C" AS c;
+SELECT * FROM mvtest_replace;
+ a  | b | c 
+----+---+---
+ 11 | 1 | y
+(1 row)
+
+-- cannot change column collation
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 12 AS a, 1 AS b, 'x' COLLATE "POSIX" AS c; -- error
+ERROR:  cannot change collation of materialized view column "c" from "C" to "POSIX"
+SELECT * FROM mvtest_replace;
+ a  | b | c 
+----+---+---
+ 11 | 1 | y
+(1 row)
+
+-- cannot drop column
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 13 AS a, 1 AS b; -- error
+ERROR:  cannot drop columns from materialized view
+SELECT * FROM mvtest_replace;
+ a  | b | c 
+----+---+---
+ 11 | 1 | y
+(1 row)
+
+-- must target a matview
+CREATE VIEW mvtest_not_mv AS
+  SELECT 1 AS a;
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_not_mv AS
+  SELECT 1 AS a; -- error
+ERROR:  "mvtest_not_mv" is not a materialized view
+DROP VIEW mvtest_not_mv;
+-- cannot use OR REPLACE with IF NOT EXISTS
+CREATE OR REPLACE MATERIALIZED VIEW IF NOT EXISTS mvtest_replace AS
+  SELECT 1 AS a;
+ERROR:  syntax error at or near "NOT"
+LINE 1: CREATE OR REPLACE MATERIALIZED VIEW IF NOT EXISTS mvtest_rep...
+                                               ^
+DROP MATERIALIZED VIEW mvtest_replace;
diff --git a/src/test/regress/sql/matview.sql b/src/test/regress/sql/matview.sql
index 6704eeae2df..fe0cb4d25bf 100644
--- a/src/test/regress/sql/matview.sql
+++ b/src/test/regress/sql/matview.sql
@@ -314,3 +314,120 @@ EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF)
   CREATE MATERIALIZED VIEW IF NOT EXISTS matview_ine_tab AS
     SELECT 1 / 0 WITH NO DATA; -- ok
 DROP MATERIALIZED VIEW matview_ine_tab;
+
+--
+-- test CREATE OR REPLACE MATERIALIZED VIEW
+--
+
+-- matview does not already exist
+DROP MATERIALIZED VIEW IF EXISTS mvtest_replace;
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 1 AS a;
+SELECT * FROM mvtest_replace;
+
+-- replace query with data
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 2 AS a;
+SELECT * FROM mvtest_replace;
+
+-- replace query without data
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 3 AS a
+  WITH NO DATA;
+SELECT * FROM mvtest_replace; -- error: not populated
+REFRESH MATERIALIZED VIEW mvtest_replace;
+SELECT * FROM mvtest_replace;
+
+-- add column
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 4 AS a, 1 b;
+SELECT * FROM mvtest_replace;
+
+-- replace table options
+SELECT m.*, c.relname, c.reloptions, s.spcname, a.amname
+  FROM mvtest_replace m
+    CROSS JOIN pg_class c
+    LEFT JOIN pg_tablespace s ON s.oid = c.reltablespace
+    LEFT JOIN pg_am a ON a.oid = c.relam
+  WHERE c.relname = 'mvtest_replace';
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace
+  USING heap2
+  WITH (fillfactor = 50)
+  TABLESPACE regress_tblspace
+  AS SELECT 5 AS a, 1 AS b;
+SELECT m.*, c.relname, c.reloptions, s.spcname, a.amname
+  FROM mvtest_replace m
+    CROSS JOIN pg_class c
+    LEFT JOIN pg_tablespace s ON s.oid = c.reltablespace
+    LEFT JOIN pg_am a ON a.oid = c.relam
+  WHERE c.relname = 'mvtest_replace';
+-- restore default options
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace
+  AS SELECT 5 AS a, 1 AS b;
+SELECT m.*, c.relname, c.reloptions, s.spcname, a.amname
+  FROM mvtest_replace m
+    CROSS JOIN pg_class c
+    LEFT JOIN pg_tablespace s ON s.oid = c.reltablespace
+    LEFT JOIN pg_am a ON a.oid = c.relam
+  WHERE c.relname = 'mvtest_replace';
+
+-- can replace matview that has a dependent view
+CREATE VIEW mvtest_replace_v AS
+  SELECT * FROM mvtest_replace;
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 6 AS a, 1 AS b;
+SELECT * FROM mvtest_replace, mvtest_replace_v;
+DROP VIEW mvtest_replace_v;
+
+-- index gets rebuilt when replacing with data
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 7 AS a, 1 AS b;
+CREATE UNIQUE INDEX ON mvtest_replace (b);
+SELECT * FROM mvtest_replace;
+SET enable_seqscan = off; -- force index scan
+EXPLAIN (COSTS OFF) SELECT * FROM mvtest_replace WHERE b = 1;
+SELECT * FROM mvtest_replace WHERE b = 1;
+RESET enable_seqscan;
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 8 AS a, 1 AS b;
+SET enable_seqscan = off; -- force index scan
+EXPLAIN (COSTS OFF) SELECT * FROM mvtest_replace WHERE b = 1;
+SELECT * FROM mvtest_replace WHERE b = 1;
+RESET enable_seqscan;
+
+-- cannot change column data type
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 9 AS a, 'x' AS b; -- error
+SELECT * FROM mvtest_replace;
+
+-- cannot rename column
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 10 AS a, 1 AS b2; -- error
+SELECT * FROM mvtest_replace;
+
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 11 AS a, 1 AS b, 'y' COLLATE "C" AS c;
+SELECT * FROM mvtest_replace;
+
+-- cannot change column collation
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 12 AS a, 1 AS b, 'x' COLLATE "POSIX" AS c; -- error
+SELECT * FROM mvtest_replace;
+
+-- cannot drop column
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 13 AS a, 1 AS b; -- error
+SELECT * FROM mvtest_replace;
+
+-- must target a matview
+CREATE VIEW mvtest_not_mv AS
+  SELECT 1 AS a;
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_not_mv AS
+  SELECT 1 AS a; -- error
+DROP VIEW mvtest_not_mv;
+
+-- cannot use OR REPLACE with IF NOT EXISTS
+CREATE OR REPLACE MATERIALIZED VIEW IF NOT EXISTS mvtest_replace AS
+  SELECT 1 AS a;
+
+DROP MATERIALIZED VIEW mvtest_replace;
-- 
2.50.1
From 21beeed4c89aa6e77e2e81de87f57cc33feaa7ef Mon Sep 17 00:00:00 2001
From: Erik Wienhold <ewie@ewie.name>
Date: Tue, 5 Aug 2025 00:05:43 +0200
Subject: [PATCH v7 2/3] Handle default tablespace in AlterTableInternal

Move handling of default tablespace for CREATE OR REPLACE MATERIALIZED
VIEW from create_ctas_internal to ATPrepSetTableSpace.  It feels cleaner
that way in my opinion by not having to resolve the tablespace name just
to pass it to AlterTableInternal.  The default table space is passed as
empty string to AlterTableInternal.
---
 src/backend/commands/createas.c  | 21 ++-------------------
 src/backend/commands/tablecmds.c | 14 ++++++++++++--
 2 files changed, 14 insertions(+), 21 deletions(-)

diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c
index 1620273f965..30ca0a21903 100644
--- a/src/backend/commands/createas.c
+++ b/src/backend/commands/createas.c
@@ -24,7 +24,6 @@
  */
 #include "postgres.h"
 
-#include "miscadmin.h"
 #include "access/heapam.h"
 #include "access/reloptions.h"
 #include "access/tableam.h"
@@ -35,7 +34,6 @@
 #include "commands/matview.h"
 #include "commands/prepare.h"
 #include "commands/tablecmds.h"
-#include "commands/tablespace.h"
 #include "commands/view.h"
 #include "executor/execdesc.h"
 #include "executor/executor.h"
@@ -160,23 +158,8 @@ create_ctas_internal(List *attrList, IntoClause *into)
 		/* tablespace */
 		atcmd = makeNode(AlterTableCmd);
 		atcmd->subtype = AT_SetTableSpace;
-		if (into->tableSpaceName != NULL)
-			atcmd->name = into->tableSpaceName;
-		else
-		{
-			Oid spcid;
-
-			/*
-			 * Resolve the name of the default or database tablespace because
-			 * we need to specify the tablespace by name.
-			 *
-			 * TODO: Move that to ATPrepSetTableSpace? Must allow AlterTableCmd.name to be NULL then.
-			 */
-			spcid = GetDefaultTablespace(RELPERSISTENCE_PERMANENT, false);
-			if (!OidIsValid(spcid))
-				spcid = MyDatabaseTableSpace;
-			atcmd->name = get_tablespace_name(spcid);
-		}
+		/* use empty string to specify default tablespace */
+		atcmd->name = into->tableSpaceName ? into->tableSpaceName : "";
 		atcmds = lappend(atcmds, atcmd);
 
 		/* storage options */
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 44dcd2c5b0d..6ec0b87f841 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -16580,8 +16580,18 @@ ATPrepSetTableSpace(AlteredTableInfo *tab, Relation rel, const char *tablespacen
 {
 	Oid			tablespaceId;
 
-	/* Check that the tablespace exists */
-	tablespaceId = get_tablespace_oid(tablespacename, false);
+	if (tablespacename != NULL && tablespacename[0] == '\0')
+	{
+		/* Use default tablespace if name is empty string */
+		tablespaceId = GetDefaultTablespace(rel->rd_rel->relpersistence, rel->rd_rel->relispartition);
+		if (!OidIsValid(tablespaceId))
+			tablespaceId = MyDatabaseTableSpace;
+	}
+	else
+	{
+		/* Check that the tablespace exists */
+		tablespaceId = get_tablespace_oid(tablespacename, false);
+	}
 
 	/* Check permissions except when moving to database's default */
 	if (OidIsValid(tablespaceId) && tablespaceId != MyDatabaseTableSpace)
-- 
2.50.1
From 01e073f5f1af495ba0559c08ad5ae1902a3ea593 Mon Sep 17 00:00:00 2001
From: Erik Wienhold <ewie@ewie.name>
Date: Fri, 26 Jul 2024 23:33:15 +0200
Subject: [PATCH v7 3/3] Add WITH OLD DATA to CREATE OR REPLACE MATERIALIZED
 VIEW

This keeps the matview populated when replacing its definition.
---
 .../sgml/ref/create_materialized_view.sgml    | 15 ++++++++--
 src/backend/commands/createas.c               | 29 +++++++++++++------
 src/backend/parser/gram.y                     | 16 ++++++++++
 src/include/nodes/primnodes.h                 |  1 +
 src/test/regress/expected/matview.out         | 22 ++++++++++++++
 src/test/regress/sql/matview.sql              | 13 +++++++++
 6 files changed, 84 insertions(+), 12 deletions(-)

diff --git a/doc/src/sgml/ref/create_materialized_view.sgml b/doc/src/sgml/ref/create_materialized_view.sgml
index 5e03320eb73..5b82af2ac70 100644
--- a/doc/src/sgml/ref/create_materialized_view.sgml
+++ b/doc/src/sgml/ref/create_materialized_view.sgml
@@ -27,7 +27,7 @@ CREATE [ OR REPLACE ] MATERIALIZED VIEW [ IF NOT EXISTS ] <replaceable>table_nam
     [ WITH ( <replaceable class="parameter">storage_parameter</replaceable> [= <replaceable class="parameter">value</replaceable>] [, ... ] ) ]
     [ TABLESPACE <replaceable class="parameter">tablespace_name</replaceable> ]
     AS <replaceable>query</replaceable>
-    [ WITH [ NO ] DATA ]
+    [ WITH [ NO | OLD ] DATA ]
 </synopsis>
  </refsynopsisdiv>
 
@@ -37,7 +37,8 @@ CREATE [ OR REPLACE ] MATERIALIZED VIEW [ IF NOT EXISTS ] <replaceable>table_nam
   <para>
    <command>CREATE MATERIALIZED VIEW</command> defines a materialized view of
    a query.  The query is executed and used to populate the view at the time
-   the command is issued (unless <command>WITH NO DATA</command> is used) and may be
+   the command is issued (unless <command>WITH NO DATA</command> or
+   <command>WITH OLD DATA</command> is used) and may be
    refreshed later using <command>REFRESH MATERIALIZED VIEW</command>.
   </para>
 
@@ -162,7 +163,7 @@ CREATE [ OR REPLACE ] MATERIALIZED VIEW [ IF NOT EXISTS ] <replaceable>table_nam
    </varlistentry>
 
    <varlistentry>
-    <term><literal>WITH [ NO ] DATA</literal></term>
+    <term><literal>WITH [ NO | OLD ] DATA</literal></term>
     <listitem>
      <para>
       This clause specifies whether or not the materialized view should be
@@ -170,6 +171,14 @@ CREATE [ OR REPLACE ] MATERIALIZED VIEW [ IF NOT EXISTS ] <replaceable>table_nam
       flagged as unscannable and cannot be queried until <command>REFRESH
       MATERIALIZED VIEW</command> is used.
      </para>
+
+     <para>
+      The form <command>WITH OLD DATA</command> keeps the already stored data
+      when replacing an existing materialized view to keep it populated.  Use
+      this form if you want to use <command>REFRESH MATERIALIZED VIEW CONCURRENTLY</command>
+      as it requires a populated materialized view.  It is an error to use this
+      form when creating a new materialized view.
+     </para>
     </listitem>
    </varlistentry>
 
diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c
index 30ca0a21903..ac2491fe01c 100644
--- a/src/backend/commands/createas.c
+++ b/src/backend/commands/createas.c
@@ -329,18 +329,26 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt,
 		/* An existing materialized view can be replaced. */
 		if (is_matview && into->replace)
 		{
-			RefreshMatViewStmt *refresh;
-
 			/* Change the relation to match the new query and other options. */
-			(void) create_ctas_nodata(query->targetList, into);
+			address = create_ctas_nodata(query->targetList, into);
+
+			/*
+			 * Refresh the materialized view with a fake statement unless we
+			 * must keep the old data.
+			 */
+			if (!into->keepData)
+			{
+				RefreshMatViewStmt *refresh;
+
+				refresh = makeNode(RefreshMatViewStmt);
+				refresh->relation = into->rel;
+				refresh->skipData = into->skipData;
+				refresh->concurrent = false;
 
-			/* Refresh the materialized view with a fake statement. */
-			refresh = makeNode(RefreshMatViewStmt);
-			refresh->relation = into->rel;
-			refresh->skipData = into->skipData;
-			refresh->concurrent = false;
+				address = ExecRefreshMatView(refresh, pstate->p_sourcetext, qc);
+			}
 
-			return ExecRefreshMatView(refresh, pstate->p_sourcetext, qc);
+			return address;
 		}
 
 		return InvalidObjectAddress;
@@ -383,6 +391,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt,
 	 */
 	if (is_matview)
 	{
+		if (into->keepData)
+			elog(ERROR, "must not specify WITH OLD DATA when creating a new materialized view");
+
 		do_refresh = !into->skipData;
 		into->skipData = true;
 	}
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 7aaf0e37ad8..e51ce2701be 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -4944,6 +4944,22 @@ CreateMatViewStmt:
 					$7->replace = true;
 					$$ = (Node *) ctas;
 				}
+		| CREATE OR REPLACE OptNoLog MATERIALIZED VIEW create_mv_target AS SelectStmt WITH OLD DATA_P
+				{
+					CreateTableAsStmt *ctas = makeNode(CreateTableAsStmt);
+
+					ctas->query = $9;
+					ctas->into = $7;
+					ctas->objtype = OBJECT_MATVIEW;
+					ctas->is_select_into = false;
+					ctas->if_not_exists = false;
+					/* cram additional flags into the IntoClause */
+					$7->rel->relpersistence = $4;
+					$7->skipData = false;
+					$7->keepData = true;
+					$7->replace = true;
+					$$ = (Node *) ctas;
+				}
 		;
 
 create_mv_target:
diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h
index 3a0c3b06c81..e4c52ab05b4 100644
--- a/src/include/nodes/primnodes.h
+++ b/src/include/nodes/primnodes.h
@@ -169,6 +169,7 @@ typedef struct IntoClause
 	/* materialized view's SELECT query */
 	struct Query *viewQuery pg_node_attr(query_jumble_ignore);
 	bool		skipData;		/* true for WITH NO DATA */
+	bool		keepData;		/* true for WITH OLD DATA */
 	bool		replace;		/* replace existing matview? */
 } IntoClause;
 
diff --git a/src/test/regress/expected/matview.out b/src/test/regress/expected/matview.out
index 56104b7ee8b..39b3a7c941c 100644
--- a/src/test/regress/expected/matview.out
+++ b/src/test/regress/expected/matview.out
@@ -731,6 +731,23 @@ SELECT * FROM mvtest_replace;
  3
 (1 row)
 
+-- replace query but keep old data
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 5 AS a
+  WITH OLD DATA;
+SELECT * FROM mvtest_replace;
+ a 
+---
+ 3
+(1 row)
+
+REFRESH MATERIALIZED VIEW mvtest_replace;
+SELECT * FROM mvtest_replace;
+ a 
+---
+ 5
+(1 row)
+
 -- add column
 CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
   SELECT 4 AS a, 1 b;
@@ -899,3 +916,8 @@ ERROR:  syntax error at or near "NOT"
 LINE 1: CREATE OR REPLACE MATERIALIZED VIEW IF NOT EXISTS mvtest_rep...
                                                ^
 DROP MATERIALIZED VIEW mvtest_replace;
+-- Clause WITH OLD DATA is not allowed when creating a new matview.
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 17 AS a
+  WITH OLD DATA; -- error
+ERROR:  must not specify WITH OLD DATA when creating a new materialized view
diff --git a/src/test/regress/sql/matview.sql b/src/test/regress/sql/matview.sql
index fe0cb4d25bf..fb2e5f4b589 100644
--- a/src/test/regress/sql/matview.sql
+++ b/src/test/regress/sql/matview.sql
@@ -338,6 +338,14 @@ SELECT * FROM mvtest_replace; -- error: not populated
 REFRESH MATERIALIZED VIEW mvtest_replace;
 SELECT * FROM mvtest_replace;
 
+-- replace query but keep old data
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 5 AS a
+  WITH OLD DATA;
+SELECT * FROM mvtest_replace;
+REFRESH MATERIALIZED VIEW mvtest_replace;
+SELECT * FROM mvtest_replace;
+
 -- add column
 CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
   SELECT 4 AS a, 1 b;
@@ -431,3 +439,8 @@ CREATE OR REPLACE MATERIALIZED VIEW IF NOT EXISTS mvtest_replace AS
   SELECT 1 AS a;
 
 DROP MATERIALIZED VIEW mvtest_replace;
+
+-- Clause WITH OLD DATA is not allowed when creating a new matview.
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 17 AS a
+  WITH OLD DATA; -- error
-- 
2.50.1

Attachments:

  [text/plain] v7-0001-Add-OR-REPLACE-option-to-CREATE-MATERIALIZED-VIEW.patch (34.9K, ../../5cd7ec92-ee61-4080-8fb6-0aed6a51eeaf@ewie.name/2-v7-0001-Add-OR-REPLACE-option-to-CREATE-MATERIALIZED-VIEW.patch)
  download | inline diff:
From 8fff179bf323ca8b4a96b977ab3193db483114cd Mon Sep 17 00:00:00 2001
From: Erik Wienhold <ewie@ewie.name>
Date: Tue, 21 May 2024 18:35:47 +0200
Subject: [PATCH v7 1/3] Add OR REPLACE option to CREATE MATERIALIZED VIEW

---
 .../sgml/ref/create_materialized_view.sgml    |  15 +-
 src/backend/commands/createas.c               | 219 ++++++++++++++----
 src/backend/commands/tablecmds.c              |   8 +-
 src/backend/commands/view.c                   | 106 ++++++---
 src/backend/parser/gram.y                     |  15 ++
 src/bin/psql/tab-complete.in.c                |  26 ++-
 src/include/commands/view.h                   |   3 +
 src/include/nodes/parsenodes.h                |   2 +-
 src/include/nodes/primnodes.h                 |   1 +
 src/test/regress/expected/matview.out         | 205 ++++++++++++++++
 src/test/regress/sql/matview.sql              | 117 ++++++++++
 11 files changed, 624 insertions(+), 93 deletions(-)

diff --git a/doc/src/sgml/ref/create_materialized_view.sgml b/doc/src/sgml/ref/create_materialized_view.sgml
index 62d897931c3..5e03320eb73 100644
--- a/doc/src/sgml/ref/create_materialized_view.sgml
+++ b/doc/src/sgml/ref/create_materialized_view.sgml
@@ -21,7 +21,7 @@ PostgreSQL documentation
 
  <refsynopsisdiv>
 <synopsis>
-CREATE MATERIALIZED VIEW [ IF NOT EXISTS ] <replaceable>table_name</replaceable>
+CREATE [ OR REPLACE ] MATERIALIZED VIEW [ IF NOT EXISTS ] <replaceable>table_name</replaceable>
     [ (<replaceable>column_name</replaceable> [, ...] ) ]
     [ USING <replaceable class="parameter">method</replaceable> ]
     [ WITH ( <replaceable class="parameter">storage_parameter</replaceable> [= <replaceable class="parameter">value</replaceable>] [, ... ] ) ]
@@ -60,6 +60,17 @@ CREATE MATERIALIZED VIEW [ IF NOT EXISTS ] <replaceable>table_name</replaceable>
   <title>Parameters</title>
 
   <variablelist>
+   <varlistentry>
+    <term><literal>OR REPLACE</literal></term>
+    <listitem>
+     <para>
+      Replaces a materialized view if it already exists.
+      Specifying <literal>OR REPLACE</literal> together with
+      <literal>IF NOT EXISTS</literal> is an error.
+     </para>
+    </listitem>
+   </varlistentry>
+
    <varlistentry>
     <term><literal>IF NOT EXISTS</literal></term>
     <listitem>
@@ -67,7 +78,7 @@ CREATE MATERIALIZED VIEW [ IF NOT EXISTS ] <replaceable>table_name</replaceable>
       Do not throw an error if a materialized view with the same name already
       exists. A notice is issued in this case.  Note that there is no guarantee
       that the existing materialized view is anything like the one that would
-      have been created.
+      have been created, unless you use <literal>OR REPLACE</literal> instead.
      </para>
     </listitem>
    </varlistentry>
diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c
index dfd2ab8e862..1620273f965 100644
--- a/src/backend/commands/createas.c
+++ b/src/backend/commands/createas.c
@@ -24,6 +24,7 @@
  */
 #include "postgres.h"
 
+#include "miscadmin.h"
 #include "access/heapam.h"
 #include "access/reloptions.h"
 #include "access/tableam.h"
@@ -34,6 +35,7 @@
 #include "commands/matview.h"
 #include "commands/prepare.h"
 #include "commands/tablecmds.h"
+#include "commands/tablespace.h"
 #include "commands/view.h"
 #include "executor/execdesc.h"
 #include "executor/executor.h"
@@ -81,55 +83,161 @@ static void intorel_destroy(DestReceiver *self);
 static ObjectAddress
 create_ctas_internal(List *attrList, IntoClause *into)
 {
-	CreateStmt *create = makeNode(CreateStmt);
-	bool		is_matview;
+	bool		is_matview,
+				replace = false;
 	char		relkind;
-	Datum		toast_options;
-	const char *const validnsps[] = HEAP_RELOPT_NAMESPACES;
+	Oid			matviewOid = InvalidOid;
 	ObjectAddress intoRelationAddr;
 
 	/* This code supports both CREATE TABLE AS and CREATE MATERIALIZED VIEW */
 	is_matview = (into->viewQuery != NULL);
 	relkind = is_matview ? RELKIND_MATVIEW : RELKIND_RELATION;
 
-	/*
-	 * Create the target relation by faking up a CREATE TABLE parsetree and
-	 * passing it to DefineRelation.
-	 */
-	create->relation = into->rel;
-	create->tableElts = attrList;
-	create->inhRelations = NIL;
-	create->ofTypename = NULL;
-	create->constraints = NIL;
-	create->options = into->options;
-	create->oncommit = into->onCommit;
-	create->tablespacename = into->tableSpaceName;
-	create->if_not_exists = false;
-	create->accessMethod = into->accessMethod;
+	/* Check if an existing materialized view needs to be replaced. */
+	if (is_matview)
+	{
+		LOCKMODE	lockmode;
 
-	/*
-	 * Create the relation.  (This will error out if there's an existing view,
-	 * so we don't need more code to complain if "replace" is false.)
-	 */
-	intoRelationAddr = DefineRelation(create, relkind, InvalidOid, NULL, NULL);
+		lockmode = into->replace ? AccessExclusiveLock : NoLock;
+		(void) RangeVarGetAndCheckCreationNamespace(into->rel, lockmode,
+													&matviewOid);
+		replace = OidIsValid(matviewOid) && into->replace;
+	}
 
-	/*
-	 * If necessary, create a TOAST table for the target table.  Note that
-	 * NewRelationCreateToastTable ends with CommandCounterIncrement(), so
-	 * that the TOAST table will be visible for insertion.
-	 */
-	CommandCounterIncrement();
+	if (is_matview && replace)
+	{
+		Relation	rel;
+		List	   *atcmds = NIL;
+		AlterTableCmd *atcmd;
+		TupleDesc	descriptor;
+
+		rel = relation_open(matviewOid, NoLock);
+
+		if (rel->rd_rel->relkind != RELKIND_MATVIEW)
+			ereport(ERROR,
+					errcode(ERRCODE_WRONG_OBJECT_TYPE),
+					errmsg("\"%s\" is not a materialized view",
+						   RelationGetRelationName(rel)));
+
+		CheckTableNotInUse(rel, "CREATE OR REPLACE MATERIALIZED VIEW");
+
+		descriptor = BuildDescForRelation(attrList);
+		checkViewColumns(descriptor, rel->rd_att, true);
+
+		/* add new attributes */
+		if (list_length(attrList) > rel->rd_att->natts)
+		{
+			ListCell   *c;
+			int			skip = rel->rd_att->natts;
+
+			foreach(c, attrList)
+			{
+				if (skip > 0)
+				{
+					skip--;
+					continue;
+				}
+				atcmd = makeNode(AlterTableCmd);
+				atcmd->subtype = AT_AddColumnToView;
+				atcmd->def = (Node *) lfirst(c);
+				atcmds = lappend(atcmds, atcmd);
+			}
+		}
+
+		/*
+		 * The following alters access method, tablespace, and storage options.
+		 * When replacing an existing matview we need to alter the relation
+		 * such that the defaults apply as if they have not been specified at
+		 * all by the CREATE statement.
+		 */
+
+		/* access method */
+		atcmd = makeNode(AlterTableCmd);
+		atcmd->subtype = AT_SetAccessMethod;
+		atcmd->name = into->accessMethod ? into->accessMethod : default_table_access_method;
+		atcmds = lappend(atcmds, atcmd);
+
+		/* tablespace */
+		atcmd = makeNode(AlterTableCmd);
+		atcmd->subtype = AT_SetTableSpace;
+		if (into->tableSpaceName != NULL)
+			atcmd->name = into->tableSpaceName;
+		else
+		{
+			Oid spcid;
+
+			/*
+			 * Resolve the name of the default or database tablespace because
+			 * we need to specify the tablespace by name.
+			 *
+			 * TODO: Move that to ATPrepSetTableSpace? Must allow AlterTableCmd.name to be NULL then.
+			 */
+			spcid = GetDefaultTablespace(RELPERSISTENCE_PERMANENT, false);
+			if (!OidIsValid(spcid))
+				spcid = MyDatabaseTableSpace;
+			atcmd->name = get_tablespace_name(spcid);
+		}
+		atcmds = lappend(atcmds, atcmd);
+
+		/* storage options */
+		atcmd = makeNode(AlterTableCmd);
+		atcmd->subtype = AT_ReplaceRelOptions;
+		atcmd->def = (Node *) into->options;
+		atcmds = lappend(atcmds, atcmd);
+
+		AlterTableInternal(matviewOid, atcmds, true);
+		CommandCounterIncrement();
+
+		relation_close(rel, NoLock);
+		ObjectAddressSet(intoRelationAddr, RelationRelationId, matviewOid);
+	}
+	else
+	{
+		CreateStmt *create = makeNode(CreateStmt);
+		Datum		toast_options;
+		const static char *validnsps[] = HEAP_RELOPT_NAMESPACES;
+
+		/*
+		 * Create the target relation by faking up a CREATE TABLE parsetree
+		 * and passing it to DefineRelation.
+		 */
+		create->relation = into->rel;
+		create->tableElts = attrList;
+		create->inhRelations = NIL;
+		create->ofTypename = NULL;
+		create->constraints = NIL;
+		create->options = into->options;
+		create->oncommit = into->onCommit;
+		create->tablespacename = into->tableSpaceName;
+		create->if_not_exists = false;
+		create->accessMethod = into->accessMethod;
+
+		/*
+		 * Create the relation.  (This will error out if there's an existing
+		 * view, so we don't need more code to complain if "replace" is
+		 * false.)
+		 */
+		intoRelationAddr = DefineRelation(create, relkind, InvalidOid, NULL,
+										  NULL);
 
-	/* parse and validate reloptions for the toast table */
-	toast_options = transformRelOptions((Datum) 0,
-										create->options,
-										"toast",
-										validnsps,
-										true, false);
+		/*
+		 * If necessary, create a TOAST table for the target table.  Note that
+		 * NewRelationCreateToastTable ends with CommandCounterIncrement(), so
+		 * that the TOAST table will be visible for insertion.
+		 */
+		CommandCounterIncrement();
+
+		/* parse and validate reloptions for the toast table */
+		toast_options = transformRelOptions((Datum) 0,
+											create->options,
+											"toast",
+											validnsps,
+											true, false);
 
-	(void) heap_reloptions(RELKIND_TOASTVALUE, toast_options, true);
+		(void) heap_reloptions(RELKIND_TOASTVALUE, toast_options, true);
 
-	NewRelationCreateToastTable(intoRelationAddr.objectId, toast_options);
+		NewRelationCreateToastTable(intoRelationAddr.objectId, toast_options);
+	}
 
 	/* Create the "view" part of a materialized view. */
 	if (is_matview)
@@ -137,7 +245,7 @@ create_ctas_internal(List *attrList, IntoClause *into)
 		/* StoreViewQuery scribbles on tree, so make a copy */
 		Query	   *query = copyObject(into->viewQuery);
 
-		StoreViewQuery(intoRelationAddr.objectId, query, false);
+		StoreViewQuery(intoRelationAddr.objectId, query, replace);
 		CommandCounterIncrement();
 	}
 
@@ -234,7 +342,26 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt,
 
 	/* Check if the relation exists or not */
 	if (CreateTableAsRelExists(stmt))
+	{
+		/* An existing materialized view can be replaced. */
+		if (is_matview && into->replace)
+		{
+			RefreshMatViewStmt *refresh;
+
+			/* Change the relation to match the new query and other options. */
+			(void) create_ctas_nodata(query->targetList, into);
+
+			/* Refresh the materialized view with a fake statement. */
+			refresh = makeNode(RefreshMatViewStmt);
+			refresh->relation = into->rel;
+			refresh->skipData = into->skipData;
+			refresh->concurrent = false;
+
+			return ExecRefreshMatView(refresh, pstate->p_sourcetext, qc);
+		}
+
 		return InvalidObjectAddress;
+	}
 
 	/*
 	 * Create the tuple receiver object and insert info it will need
@@ -402,14 +529,15 @@ CreateTableAsRelExists(CreateTableAsStmt *ctas)
 	oldrelid = get_relname_relid(into->rel->relname, nspid);
 	if (OidIsValid(oldrelid))
 	{
-		if (!ctas->if_not_exists)
+		if (!ctas->if_not_exists && !into->replace)
 			ereport(ERROR,
 					(errcode(ERRCODE_DUPLICATE_TABLE),
 					 errmsg("relation \"%s\" already exists",
 							into->rel->relname)));
 
 		/*
-		 * The relation exists and IF NOT EXISTS has been specified.
+		 * The relation exists and IF NOT EXISTS or OR REPLACE has been
+		 * specified.
 		 *
 		 * If we are in an extension script, insist that the pre-existing
 		 * object be a member of the extension, to avoid security risks.
@@ -417,11 +545,12 @@ CreateTableAsRelExists(CreateTableAsStmt *ctas)
 		ObjectAddressSet(address, RelationRelationId, oldrelid);
 		checkMembershipInCurrentExtension(&address);
 
-		/* OK to skip */
-		ereport(NOTICE,
-				(errcode(ERRCODE_DUPLICATE_TABLE),
-				 errmsg("relation \"%s\" already exists, skipping",
-						into->rel->relname)));
+		if (ctas->if_not_exists)
+			/* OK to skip */
+			ereport(NOTICE,
+					(errcode(ERRCODE_DUPLICATE_TABLE),
+					 errmsg("relation \"%s\" already exists, skipping",
+							into->rel->relname)));
 		return true;
 	}
 
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index cb811520c29..44dcd2c5b0d 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -4645,7 +4645,7 @@ AlterTableGetLockLevel(List *cmds)
 				 * Subcommands that may be visible to concurrent SELECTs
 				 */
 			case AT_DropColumn: /* change visible to SELECT */
-			case AT_AddColumnToView:	/* CREATE VIEW */
+			case AT_AddColumnToView:	/* via CREATE OR REPLACE [MATERIALIZED] VIEW */
 			case AT_DropOids:	/* used to equiv to DropColumn */
 			case AT_EnableAlwaysRule:	/* may change SELECT rules */
 			case AT_EnableReplicaRule:	/* may change SELECT rules */
@@ -4940,8 +4940,8 @@ ATPrepCmd(List **wqueue, Relation rel, AlterTableCmd *cmd,
 			/* Recursion occurs during execution phase */
 			pass = AT_PASS_ADD_COL;
 			break;
-		case AT_AddColumnToView:	/* add column via CREATE OR REPLACE VIEW */
-			ATSimplePermissions(cmd->subtype, rel, ATT_VIEW);
+		case AT_AddColumnToView:	/* via CREATE OR REPLACE [MATERIALIZED] VIEW */
+			ATSimplePermissions(cmd->subtype, rel, ATT_VIEW | ATT_MATVIEW);
 			ATPrepAddColumn(wqueue, rel, recurse, recursing, true, cmd,
 							lockmode, context);
 			/* Recursion occurs during execution phase */
@@ -5372,7 +5372,7 @@ ATExecCmd(List **wqueue, AlteredTableInfo *tab,
 	switch (cmd->subtype)
 	{
 		case AT_AddColumn:		/* ADD COLUMN */
-		case AT_AddColumnToView:	/* add column via CREATE OR REPLACE VIEW */
+		case AT_AddColumnToView:	/* via CREATE OR REPLACE [MATERIALIZED] VIEW */
 			address = ATExecAddColumn(wqueue, tab, rel, &cmd,
 									  cmd->recurse, false,
 									  lockmode, cur_pass, context);
diff --git a/src/backend/commands/view.c b/src/backend/commands/view.c
index 6f0301555e0..53d4cacc4c1 100644
--- a/src/backend/commands/view.c
+++ b/src/backend/commands/view.c
@@ -30,8 +30,6 @@
 #include "utils/lsyscache.h"
 #include "utils/rel.h"
 
-static void checkViewColumns(TupleDesc newdesc, TupleDesc olddesc);
-
 /*---------------------------------------------------------------------
  * DefineVirtualRelation
  *
@@ -129,7 +127,7 @@ DefineVirtualRelation(RangeVar *relation, List *tlist, bool replace,
 		 * column list.
 		 */
 		descriptor = BuildDescForRelation(attrList);
-		checkViewColumns(descriptor, rel->rd_att);
+		checkViewColumns(descriptor, rel->rd_att, false);
 
 		/*
 		 * If new attributes have been added, we must add pg_attribute entries
@@ -263,15 +261,22 @@ DefineVirtualRelation(RangeVar *relation, List *tlist, bool replace,
  * added to generate specific complaints.  Also, we allow the new view to have
  * more columns than the old.
  */
-static void
-checkViewColumns(TupleDesc newdesc, TupleDesc olddesc)
+void
+checkViewColumns(TupleDesc newdesc, TupleDesc olddesc, bool is_matview)
 {
 	int			i;
 
 	if (newdesc->natts < olddesc->natts)
-		ereport(ERROR,
-				(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
-				 errmsg("cannot drop columns from view")));
+	{
+		if (is_matview)
+			ereport(ERROR,
+					errcode(ERRCODE_INVALID_TABLE_DEFINITION),
+					errmsg("cannot drop columns from materialized view"));
+		else
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
+					 errmsg("cannot drop columns from view")));
+	}
 
 	for (i = 0; i < olddesc->natts; i++)
 	{
@@ -280,17 +285,34 @@ checkViewColumns(TupleDesc newdesc, TupleDesc olddesc)
 
 		/* XXX msg not right, but we don't support DROP COL on view anyway */
 		if (newattr->attisdropped != oldattr->attisdropped)
-			ereport(ERROR,
-					(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
-					 errmsg("cannot drop columns from view")));
+		{
+			if (is_matview)
+				ereport(ERROR,
+						errcode(ERRCODE_INVALID_TABLE_DEFINITION),
+						errmsg("cannot drop columns from materialized view"));
+			else
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
+						 errmsg("cannot drop columns from view")));
+		}
 
 		if (strcmp(NameStr(newattr->attname), NameStr(oldattr->attname)) != 0)
-			ereport(ERROR,
-					(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
-					 errmsg("cannot change name of view column \"%s\" to \"%s\"",
-							NameStr(oldattr->attname),
-							NameStr(newattr->attname)),
-					 errhint("Use ALTER VIEW ... RENAME COLUMN ... to change name of view column instead.")));
+		{
+			if (is_matview)
+				ereport(ERROR,
+						errcode(ERRCODE_INVALID_TABLE_DEFINITION),
+						errmsg("cannot change name of materialized view column \"%s\" to \"%s\"",
+							   NameStr(oldattr->attname),
+							   NameStr(newattr->attname)),
+						errhint("Use ALTER MATERIALIZED VIEW ... RENAME COLUMN ... to change name of materialized view column instead."));
+			else
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
+						 errmsg("cannot change name of view column \"%s\" to \"%s\"",
+								NameStr(oldattr->attname),
+								NameStr(newattr->attname)),
+						 errhint("Use ALTER VIEW ... RENAME COLUMN ... to change name of view column instead.")));
+		}
 
 		/*
 		 * We cannot allow type, typmod, or collation to change, since these
@@ -299,26 +321,48 @@ checkViewColumns(TupleDesc newdesc, TupleDesc olddesc)
 		 */
 		if (newattr->atttypid != oldattr->atttypid ||
 			newattr->atttypmod != oldattr->atttypmod)
-			ereport(ERROR,
-					(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
-					 errmsg("cannot change data type of view column \"%s\" from %s to %s",
-							NameStr(oldattr->attname),
-							format_type_with_typemod(oldattr->atttypid,
-													 oldattr->atttypmod),
-							format_type_with_typemod(newattr->atttypid,
-													 newattr->atttypmod))));
+		{
+			if (is_matview)
+				ereport(ERROR,
+						errcode(ERRCODE_INVALID_TABLE_DEFINITION),
+						errmsg("cannot change data type of materialized view column \"%s\" from %s to %s",
+							   NameStr(oldattr->attname),
+							   format_type_with_typemod(oldattr->atttypid,
+														oldattr->atttypmod),
+							   format_type_with_typemod(newattr->atttypid,
+														newattr->atttypmod)));
+			else
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
+						 errmsg("cannot change data type of view column \"%s\" from %s to %s",
+								NameStr(oldattr->attname),
+								format_type_with_typemod(oldattr->atttypid,
+														 oldattr->atttypmod),
+								format_type_with_typemod(newattr->atttypid,
+														 newattr->atttypmod))));
+		}
 
 		/*
 		 * At this point, attcollations should be both valid or both invalid,
 		 * so applying get_collation_name unconditionally should be fine.
 		 */
 		if (newattr->attcollation != oldattr->attcollation)
-			ereport(ERROR,
-					(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
-					 errmsg("cannot change collation of view column \"%s\" from \"%s\" to \"%s\"",
-							NameStr(oldattr->attname),
-							get_collation_name(oldattr->attcollation),
-							get_collation_name(newattr->attcollation))));
+		{
+			if (is_matview)
+				ereport(ERROR,
+						errcode(ERRCODE_INVALID_TABLE_DEFINITION),
+						errmsg("cannot change collation of materialized view column \"%s\" from \"%s\" to \"%s\"",
+							   NameStr(oldattr->attname),
+							   get_collation_name(oldattr->attcollation),
+							   get_collation_name(newattr->attcollation)));
+			else
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
+						 errmsg("cannot change collation of view column \"%s\" from \"%s\" to \"%s\"",
+								NameStr(oldattr->attname),
+								get_collation_name(oldattr->attcollation),
+								get_collation_name(newattr->attcollation))));
+		}
 	}
 
 	/*
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index db43034b9db..7aaf0e37ad8 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -4929,6 +4929,21 @@ CreateMatViewStmt:
 					$8->skipData = !($11);
 					$$ = (Node *) ctas;
 				}
+		| CREATE OR REPLACE OptNoLog MATERIALIZED VIEW create_mv_target AS SelectStmt opt_with_data
+				{
+					CreateTableAsStmt *ctas = makeNode(CreateTableAsStmt);
+
+					ctas->query = $9;
+					ctas->into = $7;
+					ctas->objtype = OBJECT_MATVIEW;
+					ctas->is_select_into = false;
+					ctas->if_not_exists = false;
+					/* cram additional flags into the IntoClause */
+					$7->rel->relpersistence = $4;
+					$7->skipData = !($10);
+					$7->replace = true;
+					$$ = (Node *) ctas;
+				}
 		;
 
 create_mv_target:
diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c
index 1f2ca946fc5..f49f0b60c95 100644
--- a/src/bin/psql/tab-complete.in.c
+++ b/src/bin/psql/tab-complete.in.c
@@ -2187,7 +2187,7 @@ match_previous_words(int pattern_id,
 	/* complete with something you can create or replace */
 	else if (TailMatches("CREATE", "OR", "REPLACE"))
 		COMPLETE_WITH("FUNCTION", "PROCEDURE", "LANGUAGE", "RULE", "VIEW",
-					  "AGGREGATE", "TRANSFORM", "TRIGGER");
+					  "AGGREGATE", "TRANSFORM", "TRIGGER", "MATERIALIZED VIEW");
 
 /* DROP, but not DROP embedded in other commands */
 	/* complete with something you can drop */
@@ -4072,28 +4072,34 @@ match_previous_words(int pattern_id,
 		COMPLETE_WITH("SELECT");
 
 /* CREATE MATERIALIZED VIEW */
-	else if (Matches("CREATE", "MATERIALIZED"))
+	else if (Matches("CREATE", "MATERIALIZED") ||
+			 Matches("CREATE", "OR", "REPLACE", "MATERIALIZED"))
 		COMPLETE_WITH("VIEW");
-	/* Complete CREATE MATERIALIZED VIEW <name> with AS or USING */
-	else if (Matches("CREATE", "MATERIALIZED", "VIEW", MatchAny))
+	/* Complete CREATE [ OR REPLACE ] MATERIALIZED VIEW <name> with AS or USING */
+	else if (Matches("CREATE", "MATERIALIZED", "VIEW", MatchAny) ||
+			 Matches("CREATE", "OR", "REPLACE", "MATERIALIZED", "VIEW", MatchAny))
 		COMPLETE_WITH("AS", "USING");
 
 	/*
-	 * Complete CREATE MATERIALIZED VIEW <name> USING with list of access
+	 * Complete CREATE [ OR REPLACE ] MATERIALIZED VIEW <name> USING with list of access
 	 * methods
 	 */
-	else if (Matches("CREATE", "MATERIALIZED", "VIEW", MatchAny, "USING"))
+	else if (Matches("CREATE", "MATERIALIZED", "VIEW", MatchAny, "USING") ||
+			 Matches("CREATE", "OR", "REPLACE", "MATERIALIZED", "VIEW", MatchAny, "USING"))
 		COMPLETE_WITH_QUERY(Query_for_list_of_table_access_methods);
-	/* Complete CREATE MATERIALIZED VIEW <name> USING <access method> with AS */
-	else if (Matches("CREATE", "MATERIALIZED", "VIEW", MatchAny, "USING", MatchAny))
+	/* Complete CREATE [ OR REPLACE ] MATERIALIZED VIEW <name> USING <access method> with AS */
+	else if (Matches("CREATE", "MATERIALIZED", "VIEW", MatchAny, "USING", MatchAny) ||
+			 Matches("CREATE", "OR", "REPLACE", "MATERIALIZED", "VIEW", MatchAny, "USING", MatchAny))
 		COMPLETE_WITH("AS");
 
 	/*
-	 * Complete CREATE MATERIALIZED VIEW <name> [USING <access method> ] AS
+	 * Complete CREATE [ OR REPLACE ] MATERIALIZED VIEW <name> [USING <access method> ] AS
 	 * with "SELECT"
 	 */
 	else if (Matches("CREATE", "MATERIALIZED", "VIEW", MatchAny, "AS") ||
-			 Matches("CREATE", "MATERIALIZED", "VIEW", MatchAny, "USING", MatchAny, "AS"))
+			 Matches("CREATE", "OR", "REPLACE", "MATERIALIZED", "VIEW", MatchAny, "AS") ||
+			 Matches("CREATE", "MATERIALIZED", "VIEW", MatchAny, "USING", MatchAny, "AS") ||
+			 Matches("CREATE", "OR", "REPLACE", "MATERIALIZED", "VIEW", MatchAny, "USING", MatchAny, "AS"))
 		COMPLETE_WITH("SELECT");
 
 /* CREATE EVENT TRIGGER */
diff --git a/src/include/commands/view.h b/src/include/commands/view.h
index c41f51b161c..95290f0a1c4 100644
--- a/src/include/commands/view.h
+++ b/src/include/commands/view.h
@@ -22,4 +22,7 @@ extern ObjectAddress DefineView(ViewStmt *stmt, const char *queryString,
 
 extern void StoreViewQuery(Oid viewOid, Query *viewParse, bool replace);
 
+extern void checkViewColumns(TupleDesc newdesc, TupleDesc olddesc,
+							 bool is_matview);
+
 #endif							/* VIEW_H */
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 86a236bd58b..74922f8ae89 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -2412,7 +2412,7 @@ typedef struct AlterTableStmt
 typedef enum AlterTableType
 {
 	AT_AddColumn,				/* add column */
-	AT_AddColumnToView,			/* implicitly via CREATE OR REPLACE VIEW */
+	AT_AddColumnToView,			/* implicitly via CREATE OR REPLACE [MATERIALIZED] VIEW */
 	AT_ColumnDefault,			/* alter column default */
 	AT_CookedColumnDefault,		/* add a pre-cooked column default */
 	AT_DropNotNull,				/* alter column drop not null */
diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h
index 6dfca3cb35b..3a0c3b06c81 100644
--- a/src/include/nodes/primnodes.h
+++ b/src/include/nodes/primnodes.h
@@ -169,6 +169,7 @@ typedef struct IntoClause
 	/* materialized view's SELECT query */
 	struct Query *viewQuery pg_node_attr(query_jumble_ignore);
 	bool		skipData;		/* true for WITH NO DATA */
+	bool		replace;		/* replace existing matview? */
 } IntoClause;
 
 
diff --git a/src/test/regress/expected/matview.out b/src/test/regress/expected/matview.out
index c56c9fa3a25..56104b7ee8b 100644
--- a/src/test/regress/expected/matview.out
+++ b/src/test/regress/expected/matview.out
@@ -694,3 +694,208 @@ NOTICE:  relation "matview_ine_tab" already exists, skipping
 (0 rows)
 
 DROP MATERIALIZED VIEW matview_ine_tab;
+--
+-- test CREATE OR REPLACE MATERIALIZED VIEW
+--
+-- matview does not already exist
+DROP MATERIALIZED VIEW IF EXISTS mvtest_replace;
+NOTICE:  materialized view "mvtest_replace" does not exist, skipping
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 1 AS a;
+SELECT * FROM mvtest_replace;
+ a 
+---
+ 1
+(1 row)
+
+-- replace query with data
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 2 AS a;
+SELECT * FROM mvtest_replace;
+ a 
+---
+ 2
+(1 row)
+
+-- replace query without data
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 3 AS a
+  WITH NO DATA;
+SELECT * FROM mvtest_replace; -- error: not populated
+ERROR:  materialized view "mvtest_replace" has not been populated
+HINT:  Use the REFRESH MATERIALIZED VIEW command.
+REFRESH MATERIALIZED VIEW mvtest_replace;
+SELECT * FROM mvtest_replace;
+ a 
+---
+ 3
+(1 row)
+
+-- add column
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 4 AS a, 1 b;
+SELECT * FROM mvtest_replace;
+ a | b 
+---+---
+ 4 | 1
+(1 row)
+
+-- replace table options
+SELECT m.*, c.relname, c.reloptions, s.spcname, a.amname
+  FROM mvtest_replace m
+    CROSS JOIN pg_class c
+    LEFT JOIN pg_tablespace s ON s.oid = c.reltablespace
+    LEFT JOIN pg_am a ON a.oid = c.relam
+  WHERE c.relname = 'mvtest_replace';
+ a | b |    relname     | reloptions | spcname | amname 
+---+---+----------------+------------+---------+--------
+ 4 | 1 | mvtest_replace |            |         | heap
+(1 row)
+
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace
+  USING heap2
+  WITH (fillfactor = 50)
+  TABLESPACE regress_tblspace
+  AS SELECT 5 AS a, 1 AS b;
+SELECT m.*, c.relname, c.reloptions, s.spcname, a.amname
+  FROM mvtest_replace m
+    CROSS JOIN pg_class c
+    LEFT JOIN pg_tablespace s ON s.oid = c.reltablespace
+    LEFT JOIN pg_am a ON a.oid = c.relam
+  WHERE c.relname = 'mvtest_replace';
+ a | b |    relname     |   reloptions    |     spcname      | amname 
+---+---+----------------+-----------------+------------------+--------
+ 5 | 1 | mvtest_replace | {fillfactor=50} | regress_tblspace | heap2
+(1 row)
+
+-- restore default options
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace
+  AS SELECT 5 AS a, 1 AS b;
+SELECT m.*, c.relname, c.reloptions, s.spcname, a.amname
+  FROM mvtest_replace m
+    CROSS JOIN pg_class c
+    LEFT JOIN pg_tablespace s ON s.oid = c.reltablespace
+    LEFT JOIN pg_am a ON a.oid = c.relam
+  WHERE c.relname = 'mvtest_replace';
+ a | b |    relname     | reloptions | spcname | amname 
+---+---+----------------+------------+---------+--------
+ 5 | 1 | mvtest_replace |            |         | heap
+(1 row)
+
+-- can replace matview that has a dependent view
+CREATE VIEW mvtest_replace_v AS
+  SELECT * FROM mvtest_replace;
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 6 AS a, 1 AS b;
+SELECT * FROM mvtest_replace, mvtest_replace_v;
+ a | b | a | b 
+---+---+---+---
+ 6 | 1 | 6 | 1
+(1 row)
+
+DROP VIEW mvtest_replace_v;
+-- index gets rebuilt when replacing with data
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 7 AS a, 1 AS b;
+CREATE UNIQUE INDEX ON mvtest_replace (b);
+SELECT * FROM mvtest_replace;
+ a | b 
+---+---
+ 7 | 1
+(1 row)
+
+SET enable_seqscan = off; -- force index scan
+EXPLAIN (COSTS OFF) SELECT * FROM mvtest_replace WHERE b = 1;
+                       QUERY PLAN                        
+---------------------------------------------------------
+ Index Scan using mvtest_replace_b_idx on mvtest_replace
+   Index Cond: (b = 1)
+(2 rows)
+
+SELECT * FROM mvtest_replace WHERE b = 1;
+ a | b 
+---+---
+ 7 | 1
+(1 row)
+
+RESET enable_seqscan;
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 8 AS a, 1 AS b;
+SET enable_seqscan = off; -- force index scan
+EXPLAIN (COSTS OFF) SELECT * FROM mvtest_replace WHERE b = 1;
+                       QUERY PLAN                        
+---------------------------------------------------------
+ Index Scan using mvtest_replace_b_idx on mvtest_replace
+   Index Cond: (b = 1)
+(2 rows)
+
+SELECT * FROM mvtest_replace WHERE b = 1;
+ a | b 
+---+---
+ 8 | 1
+(1 row)
+
+RESET enable_seqscan;
+-- cannot change column data type
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 9 AS a, 'x' AS b; -- error
+ERROR:  cannot change data type of materialized view column "b" from integer to text
+SELECT * FROM mvtest_replace;
+ a | b 
+---+---
+ 8 | 1
+(1 row)
+
+-- cannot rename column
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 10 AS a, 1 AS b2; -- error
+ERROR:  cannot change name of materialized view column "b" to "b2"
+HINT:  Use ALTER MATERIALIZED VIEW ... RENAME COLUMN ... to change name of materialized view column instead.
+SELECT * FROM mvtest_replace;
+ a | b 
+---+---
+ 8 | 1
+(1 row)
+
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 11 AS a, 1 AS b, 'y' COLLATE "C" AS c;
+SELECT * FROM mvtest_replace;
+ a  | b | c 
+----+---+---
+ 11 | 1 | y
+(1 row)
+
+-- cannot change column collation
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 12 AS a, 1 AS b, 'x' COLLATE "POSIX" AS c; -- error
+ERROR:  cannot change collation of materialized view column "c" from "C" to "POSIX"
+SELECT * FROM mvtest_replace;
+ a  | b | c 
+----+---+---
+ 11 | 1 | y
+(1 row)
+
+-- cannot drop column
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 13 AS a, 1 AS b; -- error
+ERROR:  cannot drop columns from materialized view
+SELECT * FROM mvtest_replace;
+ a  | b | c 
+----+---+---
+ 11 | 1 | y
+(1 row)
+
+-- must target a matview
+CREATE VIEW mvtest_not_mv AS
+  SELECT 1 AS a;
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_not_mv AS
+  SELECT 1 AS a; -- error
+ERROR:  "mvtest_not_mv" is not a materialized view
+DROP VIEW mvtest_not_mv;
+-- cannot use OR REPLACE with IF NOT EXISTS
+CREATE OR REPLACE MATERIALIZED VIEW IF NOT EXISTS mvtest_replace AS
+  SELECT 1 AS a;
+ERROR:  syntax error at or near "NOT"
+LINE 1: CREATE OR REPLACE MATERIALIZED VIEW IF NOT EXISTS mvtest_rep...
+                                               ^
+DROP MATERIALIZED VIEW mvtest_replace;
diff --git a/src/test/regress/sql/matview.sql b/src/test/regress/sql/matview.sql
index 6704eeae2df..fe0cb4d25bf 100644
--- a/src/test/regress/sql/matview.sql
+++ b/src/test/regress/sql/matview.sql
@@ -314,3 +314,120 @@ EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF)
   CREATE MATERIALIZED VIEW IF NOT EXISTS matview_ine_tab AS
     SELECT 1 / 0 WITH NO DATA; -- ok
 DROP MATERIALIZED VIEW matview_ine_tab;
+
+--
+-- test CREATE OR REPLACE MATERIALIZED VIEW
+--
+
+-- matview does not already exist
+DROP MATERIALIZED VIEW IF EXISTS mvtest_replace;
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 1 AS a;
+SELECT * FROM mvtest_replace;
+
+-- replace query with data
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 2 AS a;
+SELECT * FROM mvtest_replace;
+
+-- replace query without data
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 3 AS a
+  WITH NO DATA;
+SELECT * FROM mvtest_replace; -- error: not populated
+REFRESH MATERIALIZED VIEW mvtest_replace;
+SELECT * FROM mvtest_replace;
+
+-- add column
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 4 AS a, 1 b;
+SELECT * FROM mvtest_replace;
+
+-- replace table options
+SELECT m.*, c.relname, c.reloptions, s.spcname, a.amname
+  FROM mvtest_replace m
+    CROSS JOIN pg_class c
+    LEFT JOIN pg_tablespace s ON s.oid = c.reltablespace
+    LEFT JOIN pg_am a ON a.oid = c.relam
+  WHERE c.relname = 'mvtest_replace';
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace
+  USING heap2
+  WITH (fillfactor = 50)
+  TABLESPACE regress_tblspace
+  AS SELECT 5 AS a, 1 AS b;
+SELECT m.*, c.relname, c.reloptions, s.spcname, a.amname
+  FROM mvtest_replace m
+    CROSS JOIN pg_class c
+    LEFT JOIN pg_tablespace s ON s.oid = c.reltablespace
+    LEFT JOIN pg_am a ON a.oid = c.relam
+  WHERE c.relname = 'mvtest_replace';
+-- restore default options
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace
+  AS SELECT 5 AS a, 1 AS b;
+SELECT m.*, c.relname, c.reloptions, s.spcname, a.amname
+  FROM mvtest_replace m
+    CROSS JOIN pg_class c
+    LEFT JOIN pg_tablespace s ON s.oid = c.reltablespace
+    LEFT JOIN pg_am a ON a.oid = c.relam
+  WHERE c.relname = 'mvtest_replace';
+
+-- can replace matview that has a dependent view
+CREATE VIEW mvtest_replace_v AS
+  SELECT * FROM mvtest_replace;
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 6 AS a, 1 AS b;
+SELECT * FROM mvtest_replace, mvtest_replace_v;
+DROP VIEW mvtest_replace_v;
+
+-- index gets rebuilt when replacing with data
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 7 AS a, 1 AS b;
+CREATE UNIQUE INDEX ON mvtest_replace (b);
+SELECT * FROM mvtest_replace;
+SET enable_seqscan = off; -- force index scan
+EXPLAIN (COSTS OFF) SELECT * FROM mvtest_replace WHERE b = 1;
+SELECT * FROM mvtest_replace WHERE b = 1;
+RESET enable_seqscan;
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 8 AS a, 1 AS b;
+SET enable_seqscan = off; -- force index scan
+EXPLAIN (COSTS OFF) SELECT * FROM mvtest_replace WHERE b = 1;
+SELECT * FROM mvtest_replace WHERE b = 1;
+RESET enable_seqscan;
+
+-- cannot change column data type
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 9 AS a, 'x' AS b; -- error
+SELECT * FROM mvtest_replace;
+
+-- cannot rename column
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 10 AS a, 1 AS b2; -- error
+SELECT * FROM mvtest_replace;
+
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 11 AS a, 1 AS b, 'y' COLLATE "C" AS c;
+SELECT * FROM mvtest_replace;
+
+-- cannot change column collation
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 12 AS a, 1 AS b, 'x' COLLATE "POSIX" AS c; -- error
+SELECT * FROM mvtest_replace;
+
+-- cannot drop column
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 13 AS a, 1 AS b; -- error
+SELECT * FROM mvtest_replace;
+
+-- must target a matview
+CREATE VIEW mvtest_not_mv AS
+  SELECT 1 AS a;
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_not_mv AS
+  SELECT 1 AS a; -- error
+DROP VIEW mvtest_not_mv;
+
+-- cannot use OR REPLACE with IF NOT EXISTS
+CREATE OR REPLACE MATERIALIZED VIEW IF NOT EXISTS mvtest_replace AS
+  SELECT 1 AS a;
+
+DROP MATERIALIZED VIEW mvtest_replace;
-- 
2.50.1

  [text/plain] v7-0002-Handle-default-tablespace-in-AlterTableInternal.patch (3.1K, ../../5cd7ec92-ee61-4080-8fb6-0aed6a51eeaf@ewie.name/3-v7-0002-Handle-default-tablespace-in-AlterTableInternal.patch)
  download | inline diff:
From 21beeed4c89aa6e77e2e81de87f57cc33feaa7ef Mon Sep 17 00:00:00 2001
From: Erik Wienhold <ewie@ewie.name>
Date: Tue, 5 Aug 2025 00:05:43 +0200
Subject: [PATCH v7 2/3] Handle default tablespace in AlterTableInternal

Move handling of default tablespace for CREATE OR REPLACE MATERIALIZED
VIEW from create_ctas_internal to ATPrepSetTableSpace.  It feels cleaner
that way in my opinion by not having to resolve the tablespace name just
to pass it to AlterTableInternal.  The default table space is passed as
empty string to AlterTableInternal.
---
 src/backend/commands/createas.c  | 21 ++-------------------
 src/backend/commands/tablecmds.c | 14 ++++++++++++--
 2 files changed, 14 insertions(+), 21 deletions(-)

diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c
index 1620273f965..30ca0a21903 100644
--- a/src/backend/commands/createas.c
+++ b/src/backend/commands/createas.c
@@ -24,7 +24,6 @@
  */
 #include "postgres.h"
 
-#include "miscadmin.h"
 #include "access/heapam.h"
 #include "access/reloptions.h"
 #include "access/tableam.h"
@@ -35,7 +34,6 @@
 #include "commands/matview.h"
 #include "commands/prepare.h"
 #include "commands/tablecmds.h"
-#include "commands/tablespace.h"
 #include "commands/view.h"
 #include "executor/execdesc.h"
 #include "executor/executor.h"
@@ -160,23 +158,8 @@ create_ctas_internal(List *attrList, IntoClause *into)
 		/* tablespace */
 		atcmd = makeNode(AlterTableCmd);
 		atcmd->subtype = AT_SetTableSpace;
-		if (into->tableSpaceName != NULL)
-			atcmd->name = into->tableSpaceName;
-		else
-		{
-			Oid spcid;
-
-			/*
-			 * Resolve the name of the default or database tablespace because
-			 * we need to specify the tablespace by name.
-			 *
-			 * TODO: Move that to ATPrepSetTableSpace? Must allow AlterTableCmd.name to be NULL then.
-			 */
-			spcid = GetDefaultTablespace(RELPERSISTENCE_PERMANENT, false);
-			if (!OidIsValid(spcid))
-				spcid = MyDatabaseTableSpace;
-			atcmd->name = get_tablespace_name(spcid);
-		}
+		/* use empty string to specify default tablespace */
+		atcmd->name = into->tableSpaceName ? into->tableSpaceName : "";
 		atcmds = lappend(atcmds, atcmd);
 
 		/* storage options */
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 44dcd2c5b0d..6ec0b87f841 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -16580,8 +16580,18 @@ ATPrepSetTableSpace(AlteredTableInfo *tab, Relation rel, const char *tablespacen
 {
 	Oid			tablespaceId;
 
-	/* Check that the tablespace exists */
-	tablespaceId = get_tablespace_oid(tablespacename, false);
+	if (tablespacename != NULL && tablespacename[0] == '\0')
+	{
+		/* Use default tablespace if name is empty string */
+		tablespaceId = GetDefaultTablespace(rel->rd_rel->relpersistence, rel->rd_rel->relispartition);
+		if (!OidIsValid(tablespaceId))
+			tablespaceId = MyDatabaseTableSpace;
+	}
+	else
+	{
+		/* Check that the tablespace exists */
+		tablespaceId = get_tablespace_oid(tablespacename, false);
+	}
 
 	/* Check permissions except when moving to database's default */
 	if (OidIsValid(tablespaceId) && tablespaceId != MyDatabaseTableSpace)
-- 
2.50.1

  [text/plain] v7-0003-Add-WITH-OLD-DATA-to-CREATE-OR-REPLACE-MATERIALIZ.patch (8.0K, ../../5cd7ec92-ee61-4080-8fb6-0aed6a51eeaf@ewie.name/4-v7-0003-Add-WITH-OLD-DATA-to-CREATE-OR-REPLACE-MATERIALIZ.patch)
  download | inline diff:
From 01e073f5f1af495ba0559c08ad5ae1902a3ea593 Mon Sep 17 00:00:00 2001
From: Erik Wienhold <ewie@ewie.name>
Date: Fri, 26 Jul 2024 23:33:15 +0200
Subject: [PATCH v7 3/3] Add WITH OLD DATA to CREATE OR REPLACE MATERIALIZED
 VIEW

This keeps the matview populated when replacing its definition.
---
 .../sgml/ref/create_materialized_view.sgml    | 15 ++++++++--
 src/backend/commands/createas.c               | 29 +++++++++++++------
 src/backend/parser/gram.y                     | 16 ++++++++++
 src/include/nodes/primnodes.h                 |  1 +
 src/test/regress/expected/matview.out         | 22 ++++++++++++++
 src/test/regress/sql/matview.sql              | 13 +++++++++
 6 files changed, 84 insertions(+), 12 deletions(-)

diff --git a/doc/src/sgml/ref/create_materialized_view.sgml b/doc/src/sgml/ref/create_materialized_view.sgml
index 5e03320eb73..5b82af2ac70 100644
--- a/doc/src/sgml/ref/create_materialized_view.sgml
+++ b/doc/src/sgml/ref/create_materialized_view.sgml
@@ -27,7 +27,7 @@ CREATE [ OR REPLACE ] MATERIALIZED VIEW [ IF NOT EXISTS ] <replaceable>table_nam
     [ WITH ( <replaceable class="parameter">storage_parameter</replaceable> [= <replaceable class="parameter">value</replaceable>] [, ... ] ) ]
     [ TABLESPACE <replaceable class="parameter">tablespace_name</replaceable> ]
     AS <replaceable>query</replaceable>
-    [ WITH [ NO ] DATA ]
+    [ WITH [ NO | OLD ] DATA ]
 </synopsis>
  </refsynopsisdiv>
 
@@ -37,7 +37,8 @@ CREATE [ OR REPLACE ] MATERIALIZED VIEW [ IF NOT EXISTS ] <replaceable>table_nam
   <para>
    <command>CREATE MATERIALIZED VIEW</command> defines a materialized view of
    a query.  The query is executed and used to populate the view at the time
-   the command is issued (unless <command>WITH NO DATA</command> is used) and may be
+   the command is issued (unless <command>WITH NO DATA</command> or
+   <command>WITH OLD DATA</command> is used) and may be
    refreshed later using <command>REFRESH MATERIALIZED VIEW</command>.
   </para>
 
@@ -162,7 +163,7 @@ CREATE [ OR REPLACE ] MATERIALIZED VIEW [ IF NOT EXISTS ] <replaceable>table_nam
    </varlistentry>
 
    <varlistentry>
-    <term><literal>WITH [ NO ] DATA</literal></term>
+    <term><literal>WITH [ NO | OLD ] DATA</literal></term>
     <listitem>
      <para>
       This clause specifies whether or not the materialized view should be
@@ -170,6 +171,14 @@ CREATE [ OR REPLACE ] MATERIALIZED VIEW [ IF NOT EXISTS ] <replaceable>table_nam
       flagged as unscannable and cannot be queried until <command>REFRESH
       MATERIALIZED VIEW</command> is used.
      </para>
+
+     <para>
+      The form <command>WITH OLD DATA</command> keeps the already stored data
+      when replacing an existing materialized view to keep it populated.  Use
+      this form if you want to use <command>REFRESH MATERIALIZED VIEW CONCURRENTLY</command>
+      as it requires a populated materialized view.  It is an error to use this
+      form when creating a new materialized view.
+     </para>
     </listitem>
    </varlistentry>
 
diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c
index 30ca0a21903..ac2491fe01c 100644
--- a/src/backend/commands/createas.c
+++ b/src/backend/commands/createas.c
@@ -329,18 +329,26 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt,
 		/* An existing materialized view can be replaced. */
 		if (is_matview && into->replace)
 		{
-			RefreshMatViewStmt *refresh;
-
 			/* Change the relation to match the new query and other options. */
-			(void) create_ctas_nodata(query->targetList, into);
+			address = create_ctas_nodata(query->targetList, into);
+
+			/*
+			 * Refresh the materialized view with a fake statement unless we
+			 * must keep the old data.
+			 */
+			if (!into->keepData)
+			{
+				RefreshMatViewStmt *refresh;
+
+				refresh = makeNode(RefreshMatViewStmt);
+				refresh->relation = into->rel;
+				refresh->skipData = into->skipData;
+				refresh->concurrent = false;
 
-			/* Refresh the materialized view with a fake statement. */
-			refresh = makeNode(RefreshMatViewStmt);
-			refresh->relation = into->rel;
-			refresh->skipData = into->skipData;
-			refresh->concurrent = false;
+				address = ExecRefreshMatView(refresh, pstate->p_sourcetext, qc);
+			}
 
-			return ExecRefreshMatView(refresh, pstate->p_sourcetext, qc);
+			return address;
 		}
 
 		return InvalidObjectAddress;
@@ -383,6 +391,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt,
 	 */
 	if (is_matview)
 	{
+		if (into->keepData)
+			elog(ERROR, "must not specify WITH OLD DATA when creating a new materialized view");
+
 		do_refresh = !into->skipData;
 		into->skipData = true;
 	}
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 7aaf0e37ad8..e51ce2701be 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -4944,6 +4944,22 @@ CreateMatViewStmt:
 					$7->replace = true;
 					$$ = (Node *) ctas;
 				}
+		| CREATE OR REPLACE OptNoLog MATERIALIZED VIEW create_mv_target AS SelectStmt WITH OLD DATA_P
+				{
+					CreateTableAsStmt *ctas = makeNode(CreateTableAsStmt);
+
+					ctas->query = $9;
+					ctas->into = $7;
+					ctas->objtype = OBJECT_MATVIEW;
+					ctas->is_select_into = false;
+					ctas->if_not_exists = false;
+					/* cram additional flags into the IntoClause */
+					$7->rel->relpersistence = $4;
+					$7->skipData = false;
+					$7->keepData = true;
+					$7->replace = true;
+					$$ = (Node *) ctas;
+				}
 		;
 
 create_mv_target:
diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h
index 3a0c3b06c81..e4c52ab05b4 100644
--- a/src/include/nodes/primnodes.h
+++ b/src/include/nodes/primnodes.h
@@ -169,6 +169,7 @@ typedef struct IntoClause
 	/* materialized view's SELECT query */
 	struct Query *viewQuery pg_node_attr(query_jumble_ignore);
 	bool		skipData;		/* true for WITH NO DATA */
+	bool		keepData;		/* true for WITH OLD DATA */
 	bool		replace;		/* replace existing matview? */
 } IntoClause;
 
diff --git a/src/test/regress/expected/matview.out b/src/test/regress/expected/matview.out
index 56104b7ee8b..39b3a7c941c 100644
--- a/src/test/regress/expected/matview.out
+++ b/src/test/regress/expected/matview.out
@@ -731,6 +731,23 @@ SELECT * FROM mvtest_replace;
  3
 (1 row)
 
+-- replace query but keep old data
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 5 AS a
+  WITH OLD DATA;
+SELECT * FROM mvtest_replace;
+ a 
+---
+ 3
+(1 row)
+
+REFRESH MATERIALIZED VIEW mvtest_replace;
+SELECT * FROM mvtest_replace;
+ a 
+---
+ 5
+(1 row)
+
 -- add column
 CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
   SELECT 4 AS a, 1 b;
@@ -899,3 +916,8 @@ ERROR:  syntax error at or near "NOT"
 LINE 1: CREATE OR REPLACE MATERIALIZED VIEW IF NOT EXISTS mvtest_rep...
                                                ^
 DROP MATERIALIZED VIEW mvtest_replace;
+-- Clause WITH OLD DATA is not allowed when creating a new matview.
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 17 AS a
+  WITH OLD DATA; -- error
+ERROR:  must not specify WITH OLD DATA when creating a new materialized view
diff --git a/src/test/regress/sql/matview.sql b/src/test/regress/sql/matview.sql
index fe0cb4d25bf..fb2e5f4b589 100644
--- a/src/test/regress/sql/matview.sql
+++ b/src/test/regress/sql/matview.sql
@@ -338,6 +338,14 @@ SELECT * FROM mvtest_replace; -- error: not populated
 REFRESH MATERIALIZED VIEW mvtest_replace;
 SELECT * FROM mvtest_replace;
 
+-- replace query but keep old data
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 5 AS a
+  WITH OLD DATA;
+SELECT * FROM mvtest_replace;
+REFRESH MATERIALIZED VIEW mvtest_replace;
+SELECT * FROM mvtest_replace;
+
 -- add column
 CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
   SELECT 4 AS a, 1 b;
@@ -431,3 +439,8 @@ CREATE OR REPLACE MATERIALIZED VIEW IF NOT EXISTS mvtest_replace AS
   SELECT 1 AS a;
 
 DROP MATERIALIZED VIEW mvtest_replace;
+
+-- Clause WITH OLD DATA is not allowed when creating a new matview.
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 17 AS a
+  WITH OLD DATA; -- error
-- 
2.50.1

^ permalink  raw  reply  [nested|flat] 29+ messages in thread

* Re: CREATE OR REPLACE MATERIALIZED VIEW
@ 2026-02-03 00:19  Paul A Jungwirth <pj@illuminatedcomputing.com>
  parent: Erik Wienhold <ewie@ewie.name>
  1 sibling, 1 reply; 29+ messages in thread

From: Paul A Jungwirth @ 2026-02-03 00:19 UTC (permalink / raw)
  To: Erik Wienhold <ewie@ewie.name>; +Cc: Tom Lane <tgl@sss.pgh.pa.us>; Said Assemlal <sassemlal@neurorx.com>; pgsql-hackers@postgresql.org, Haibo Yan <haibo.yan@hotmail.com>

Hello,

Yan Haibo and I reviewed this patch for the Patch Review Workshop.

The patch applies, compiles, and passes tests.

From v7-0001-Add-OR-REPLACE-option-to-CREATE-MATERIALIZED-VIEW.patch:

```
diff --git a/doc/src/sgml/ref/create_materialized_view.sgml
b/doc/src/sgml/ref/create_materialized_view.sgml
index 62d897931c3..5e03320eb73 100644
--- a/doc/src/sgml/ref/create_materialized_view.sgml
+++ b/doc/src/sgml/ref/create_materialized_view.sgml
@@ -67,7 +78,7 @@ CREATE MATERIALIZED VIEW [ IF NOT EXISTS ]
<replaceable>table_name</replaceable>
       Do not throw an error if a materialized view with the same name already
       exists. A notice is issued in this case.  Note that there is no guarantee
       that the existing materialized view is anything like the one that would
-      have been created.
+      have been created, unless you use <literal>OR REPLACE</literal> instead.
      </para>
     </listitem>
    </varlistentry>
```

"Unless" flows strangely here, at first connoting that it will work
with IF NOT EXISTS, but then "instead" retracts that suggestion.
Perhaps "unlike when using OR REPLACE".

```
 static ObjectAddress
 create_ctas_internal(List *attrList, IntoClause *into)
 {
-   CreateStmt *create = makeNode(CreateStmt);
-   bool        is_matview;
+   bool        is_matview,
+               replace = false;
    char        relkind;
-   Datum       toast_options;
-   const char *const validnsps[] = HEAP_RELOPT_NAMESPACES;
+   Oid         matviewOid = InvalidOid;
    ObjectAddress intoRelationAddr;

    /* This code supports both CREATE TABLE AS and CREATE MATERIALIZED VIEW */
    is_matview = (into->viewQuery != NULL);
    relkind = is_matview ? RELKIND_MATVIEW : RELKIND_RELATION;

-   /*
-    * Create the target relation by faking up a CREATE TABLE parsetree and
-    * passing it to DefineRelation.
-    */
-   create->relation = into->rel;
-   create->tableElts = attrList;
-   create->inhRelations = NIL;
-   create->ofTypename = NULL;
-   create->constraints = NIL;
-   create->options = into->options;
-   create->oncommit = into->onCommit;
-   create->tablespacename = into->tableSpaceName;
-   create->if_not_exists = false;
-   create->accessMethod = into->accessMethod;
```

It seems like there is very little shared in this function between the
old code and new. Should replacing a matview just have a different
function altogether?

```
+   /* Check if an existing materialized view needs to be replaced. */
+   if (is_matview)
+   {
+       LOCKMODE    lockmode;

-   /*
-    * Create the relation.  (This will error out if there's an existing view,
-    * so we don't need more code to complain if "replace" is false.)
-    */
-   intoRelationAddr = DefineRelation(create, relkind, InvalidOid, NULL, NULL);
+       lockmode = into->replace ? AccessExclusiveLock : NoLock;
+       (void) RangeVarGetAndCheckCreationNamespace(into->rel, lockmode,
+                                                   &matviewOid);
+       replace = OidIsValid(matviewOid) && into->replace;
+   }
```

We didn't call RangeVarGetAndCheckCreationNamespace before, but now we
call it whether you say `OR REPLACE` or not. That seems suspicious.
Why do we need to call it even when you don't ask to replace an
existing matview? We only use matviewOid if we're replacing, and we
only lock the relation if we're replacing. So probably this should be
`if (is_matview && into->replace)` (which makes this function seem
even more strongly that it should be split into two).

```
+       rel = relation_open(matviewOid, NoLock);
```

The stanzas in this branch need comments. The code strongly resembles
DefineVirtualRelation, but without the comments. For instance in this
line, I wasn't sure why were opening the relation with no lock. This
comment from DefineVirtualRelation is very helpful:

```
/* Relation is already locked, but we must build a relcache entry. */`
```

The other lost comments would be helpful too.

```
+       /* add new attributes */
+       if (list_length(attrList) > rel->rd_att->natts)
+       {
+           ListCell   *c;
+           int         skip = rel->rd_att->natts;
+
+           foreach(c, attrList)
+           {
+               if (skip > 0)
+               {
+                   skip--;
+                   continue;
+               }
+               atcmd = makeNode(AlterTableCmd);
+               atcmd->subtype = AT_AddColumnToView;
+               atcmd->def = (Node *) lfirst(c);
+               atcmds = lappend(atcmds, atcmd);
+           }
+       }
```

Can we use list_nth_cell to start at the right position and avoid the
manual skipping? (So the loop would be for instead of foreach.) Or
even better, for_each_from looks perfect for this.

```
+       /* access method */
+       atcmd = makeNode(AlterTableCmd);
+       atcmd->subtype = AT_SetAccessMethod;
+       atcmd->name = into->accessMethod ? into->accessMethod :
default_table_access_method;
+       atcmds = lappend(atcmds, atcmd);
```

Just to confirm, this is a noop if the access method is already the
same? And likewise with the other AT subcommands?

```
@@ -234,7 +342,26 @@ ExecCreateTableAs(ParseState *pstate,
CreateTableAsStmt *stmt,

    /* Check if the relation exists or not */
    if (CreateTableAsRelExists(stmt))
+   {
+       /* An existing materialized view can be replaced. */
+       if (is_matview && into->replace)
+       {
+           RefreshMatViewStmt *refresh;
+
+           /* Change the relation to match the new query and other options. */
+           (void) create_ctas_nodata(query->targetList, into);
+
+           /* Refresh the materialized view with a fake statement. */
+           refresh = makeNode(RefreshMatViewStmt);
+           refresh->relation = into->rel;
+           refresh->skipData = into->skipData;
+           refresh->concurrent = false;
+
+           return ExecRefreshMatView(refresh, pstate->p_sourcetext, qc);
+       }
+
        return InvalidObjectAddress;
+   }
```

Maybe an `else` for the other case? Having parallel indentation
communicates the parallel execution.

```
@@ -402,14 +529,15 @@ CreateTableAsRelExists(CreateTableAsStmt *ctas)
    oldrelid = get_relname_relid(into->rel->relname, nspid);
    if (OidIsValid(oldrelid))
    {
-       if (!ctas->if_not_exists)
+       if (!ctas->if_not_exists && !into->replace)
```

Changing the contract without changing the function comment seems wrong.

But is this really factored correctly? Maybe the check should happen
in the caller instead.

Does this require a change to ExplainOneUtility, which also calls this
function? At least we probably need to handle OR REPLACE there.

```
    if (newdesc->natts < olddesc->natts)
-       ereport(ERROR,
-               (errcode(ERRCODE_INVALID_TABLE_DEFINITION),
-                errmsg("cannot drop columns from view")));
+   {
+       if (is_matview)
+           ereport(ERROR,
+                   errcode(ERRCODE_INVALID_TABLE_DEFINITION),
+                   errmsg("cannot drop columns from materialized view"));
+       else
+           ereport(ERROR,
+                   (errcode(ERRCODE_INVALID_TABLE_DEFINITION),
+                    errmsg("cannot drop columns from view")));
+   }
```

Instead of repeating so much, perhaps `errmsg("cannot drop columns
from %s", is_matview ? "materialized view" : "view")`? Or since that
is probably bad for translators, at least this:

```
errmsg(is_matview
       ? "cannot drop columns from materialized view"
       : "cannot drop columns from view")
```

Likewise with more error messages in this function. (There are a lot of them.)

The parser changes seem fine (see below re the third patch though).

Tab completion seems fine.

From v7-0002-Handle-default-tablespace-in-AlterTableInternal.patch:

These changes should get rolled into the previous patch. I think you
broke them out to allow considering them separately, although your
patches would be simpler if they were the first patch in the series
then.

```
+       /* use empty string to specify default tablespace */
+       atcmd->name = into->tableSpaceName ? into->tableSpaceName : "";
        atcmds = lappend(atcmds, atcmd);
```

We didn't love using "" as a magic string to signal the
AT_SetTableSpace subcommand. At least it should be a named constant,
but maybe a separate struct member is better.

```
-   /* Check that the tablespace exists */
-   tablespaceId = get_tablespace_oid(tablespacename, false);
+   if (tablespacename != NULL && tablespacename[0] == '\0')
+   {
+       /* Use default tablespace if name is empty string */
+       tablespaceId =
GetDefaultTablespace(rel->rd_rel->relpersistence,
rel->rd_rel->relispartition);
+       if (!OidIsValid(tablespaceId))
+           tablespaceId = MyDatabaseTableSpace;
+   }
+   else
+   {
+       /* Check that the tablespace exists */
+       tablespaceId = get_tablespace_oid(tablespacename, false);
+   }

    /* Check permissions except when moving to database's default */
    if (OidIsValid(tablespaceId) && tablespaceId != MyDatabaseTableSpace)
```

This seems like something that should happen during analysis, but I
know we are less strict for utility statements.

Are there any locking issues with calling GetDefaultTablespace here
instead of earlier? Why does ATPrepSetTableSpace take a lockmode but
not use it? Can the table space get dropped in the middle of the
command? But this is probably handled already. Do we need the lock the
*old* tablespace?

What about partitions?

From v7-0003-Add-WITH-OLD-DATA-to-CREATE-OR-REPLACE-MATERIALIZ.patch:

```
@@ -383,6 +391,9 @@ ExecCreateTableAs(ParseState *pstate,
CreateTableAsStmt *stmt,
     */
    if (is_matview)
    {
+       if (into->keepData)
+           elog(ERROR, "must not specify WITH OLD DATA when creating
a new materialized view");
+
        do_refresh = !into->skipData;
        into->skipData = true;
    }
```

Should this be ereport? I guess the current parser productions should
prevent us from reaching this code though. So perhaps a comment?

```
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 7aaf0e37ad8..e51ce2701be 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -4944,6 +4944,22 @@ CreateMatViewStmt:
                    $7->replace = true;
                    $$ = (Node *) ctas;
                }
+       | CREATE OR REPLACE OptNoLog MATERIALIZED VIEW
create_mv_target AS SelectStmt WITH OLD DATA_P
+               {
+                   CreateTableAsStmt *ctas = makeNode(CreateTableAsStmt);
+
+                   ctas->query = $9;
+                   ctas->into = $7;
+                   ctas->objtype = OBJECT_MATVIEW;
+                   ctas->is_select_into = false;
+                   ctas->if_not_exists = false;
+                   /* cram additional flags into the IntoClause */
+                   $7->rel->relpersistence = $4;
+                   $7->skipData = false;
+                   $7->keepData = true;
+                   $7->replace = true;
+                   $$ = (Node *) ctas;
+               }
        ;
```

This is the fourth production for minor variations of
CreateMatViewStmt, which seems like a lot. Perhaps instead of
opt_with_data we add a new production opt_with_no_or_old_data that can
return 3 alteratives (WITH [{NO|OLD}] DATA)?

```
+-- replace query but keep old data
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 5 AS a
+  WITH OLD DATA;
+SELECT * FROM mvtest_replace;
+REFRESH MATERIALIZED VIEW mvtest_replace;
+SELECT * FROM mvtest_replace;
+
```

We would like to see a test adding a new column while using WITH OLD
DATA. It is safe because every heap tuple knows how many attributes it
holds (and I tested it), but I did wonder if anyone had been tempted
to call fastgetattr directly for matviews. (Before your feature, it
would have been safe, I think.) Here is the test I did:

```
create materialized view mvnulls as
  select 1 c1, 2 c2, 3 c3, 4 c4, 5 c5, 6 c6, 7 c7, null c8;
-- add a ninth column (exceeding the old t_bits):
create or replace materialized view mvnulls as
  select 1 c1, 2 c2, 3 c3, 4 c4, 5 c5, 6 c6, 7 c7, null c8, null c9
with old data;
select c9 from mvnulls;
```

Another thing to think about and test: what if while using WITH OLD
DATA the new matview definition violates a unique index? Or a domain?
Are there any other constraints that can be applied to matviews to
consider?

Yours,

-- 
Paul              ~{:-)
pj@illuminatedcomputing.com





^ permalink  raw  reply  [nested|flat] 29+ messages in thread

* Re: CREATE OR REPLACE MATERIALIZED VIEW
@ 2026-05-01 15:59  Soumya S Murali <soumyamurali.work@gmail.com>
  parent: Erik Wienhold <ewie@ewie.name>
  1 sibling, 1 reply; 29+ messages in thread

From: Soumya S Murali @ 2026-05-01 15:59 UTC (permalink / raw)
  To: Erik Wienhold <ewie@ewie.name>; +Cc: Tom Lane <tgl@sss.pgh.pa.us>; Said Assemlal <sassemlal@neurorx.com>; pgsql-hackers

Hi all,


On Tue, Apr 21, 2026 at 11:56 AM Erik Wienhold <ewie@ewie.name> wrote:
>
> Sorry for the late reply but I haven't had the time to dig into this.
> Here's v7 fixing the points below.
>
> On 2025-04-05 22:37 +0200, Tom Lane wrote:
> > * I think the proposal to deprecate IF NOT EXISTS is a nonstarter.
> > Yeah, I don't like it much, but the standard of proof to remove
> > features is amazingly high and I don't think it's been reached here.
> > We're unlikely to remove IF NOT EXISTS for tables, and to the extent
> > that matviews are like tables it's reasonable for them to have it too.
>
> Yeah, I got that gist from the replies upthread and dropped that patch.
>
> > * On the other hand, the semantics you've implemented for CREATE OR
> > REPLACE are not right.  The contract for any form of C.O.R. is that
> > it will either fail, or produce exactly the same object definition
> > that you would have gotten from plain CREATE with no conflicting
> > object.  The v6 code is visibly not doing that for properties such
> > as tablespace --- if the command doesn't mention that, you don't
> > get the default tablespace, you get whatever the old object had.
>
> Thanks a lot.  I added a test case for that and v7-0001 now restores the
> default options if none are specified.  Handling the default tablespace
> is a bit cumbersome IMO because its name must be passed to
> AlterTableInternal.  With v7-0002 I moved that to ATPrepSetTableSpace as
> an alternative using the empty string as stand-in for the default
> tablespace.  What do you think?
>
> > * BTW, I'm inclined to think that WITH OLD DATA ought to fail
> > if the command isn't replacing an existing matview.  It seems
> > inconsistent to silently reinterpret it as WITH DATA, just as
> > silently reinterpreting "no tablespace mentioned" as "use the
> > old tablespace" is inconsistent.  I'm not dead set on that
> > but it feels wrong.
>
> Yes that also felt iffy to me.  It just didn't occur to me to simply
> raise an error in ExecCreateTableAs.  Done so in v7-0003.


I tested the patches and the feature is working as expected, I am able
to create and replace a materialized view successfully and the data
updates correctly after replacement. Replacing multiple times also
works without any issues. Also I observed that adding new columns
during replacement works fine but removing existing columns is not
allowed, and it throws an error. This behavior seems correct and is
similar to how normal views work. In cases where dependencies exist,
the system prevents unsafe changes through these validations. Also,
REFRESH MATERIALIZED VIEW works properly after replacement.

But a few limitations (if I am not wrong) are noticed. I am attaching
the outputs here:-
1. CREATE OR REPLACE MATERIALIZED VIEW mv_test AS SELECT
generate_series(1,5) AS a WITH NO DATA;) is not currently supported
with the replace option and results in an error during validation.
postgres=# CREATE OR REPLACE MATERIALIZED VIEW mv_test AS
SELECT generate_series(1,5) AS a WITH NO DATA;
ERROR:  cannot drop columns from materialized view

2. When a dependent materialized view exists, replacing the base view
fails due to column validation errors.
postgres=# CREATE MATERIALIZED VIEW mv_table AS SELECT * FROM mv_test;
SELECT 1
postgres=# CREATE OR REPLACE MATERIALIZED VIEW mv_test AS SELECT 100 AS a;
ERROR:  cannot drop columns from materialized view
Here I think the system is not handling dependencies directly instead
blocking changes indirectly due to column mismatch.

3. Schema evolution is limited (cannot drop or modify existing columns).
postgres=# CREATE OR REPLACE MATERIALIZED VIEW mv_test AS SELECT 10 AS a;
ERROR:  cannot drop columns from materialized view

Overall, the implementation is stable and follows safe behavior.
Please let me know if I should explore handling the remaining
limitations or add more test cases.

Regards,
Soumya





^ permalink  raw  reply  [nested|flat] 29+ messages in thread

* Re: CREATE OR REPLACE MATERIALIZED VIEW
@ 2026-06-28 18:14  Erik Wienhold <ewie@ewie.name>
  parent: Paul A Jungwirth <pj@illuminatedcomputing.com>
  0 siblings, 1 reply; 29+ messages in thread

From: Erik Wienhold @ 2026-06-28 18:14 UTC (permalink / raw)
  To: Paul A Jungwirth <pj@illuminatedcomputing.com>; +Cc: Tom Lane <tgl@sss.pgh.pa.us>; Said Assemlal <sassemlal@neurorx.com>; pgsql-hackers@postgresql.org, Haibo Yan <haibo.yan@hotmail.com>


On 2026-02-03 01:19 +0100, Paul A Jungwirth wrote:
> Yan Haibo and I reviewed this patch for the Patch Review Workshop.

Thank you both for your review.  Sorry about my late reply but it took
me some time to address your points with the attached v8 and I only had
so little time available.

> The patch applies, compiles, and passes tests.
> 
> From v7-0001-Add-OR-REPLACE-option-to-CREATE-MATERIALIZED-VIEW.patch:
> 
> ```
> diff --git a/doc/src/sgml/ref/create_materialized_view.sgml
> b/doc/src/sgml/ref/create_materialized_view.sgml
> index 62d897931c3..5e03320eb73 100644
> --- a/doc/src/sgml/ref/create_materialized_view.sgml
> +++ b/doc/src/sgml/ref/create_materialized_view.sgml
> @@ -67,7 +78,7 @@ CREATE MATERIALIZED VIEW [ IF NOT EXISTS ]
> <replaceable>table_name</replaceable>
>        Do not throw an error if a materialized view with the same name already
>        exists. A notice is issued in this case.  Note that there is no guarantee
>        that the existing materialized view is anything like the one that would
> -      have been created.
> +      have been created, unless you use <literal>OR REPLACE</literal> instead.
>       </para>
>      </listitem>
>     </varlistentry>
> ```
> 
> "Unless" flows strangely here, at first connoting that it will work
> with IF NOT EXISTS, but then "instead" retracts that suggestion.
> Perhaps "unlike when using OR REPLACE".

Done.

> ```
>  static ObjectAddress
>  create_ctas_internal(List *attrList, IntoClause *into)
>  {
> -   CreateStmt *create = makeNode(CreateStmt);
> -   bool        is_matview;
> +   bool        is_matview,
> +               replace = false;
>     char        relkind;
> -   Datum       toast_options;
> -   const char *const validnsps[] = HEAP_RELOPT_NAMESPACES;
> +   Oid         matviewOid = InvalidOid;
>     ObjectAddress intoRelationAddr;
> 
>     /* This code supports both CREATE TABLE AS and CREATE MATERIALIZED VIEW */
>     is_matview = (into->viewQuery != NULL);
>     relkind = is_matview ? RELKIND_MATVIEW : RELKIND_RELATION;
> 
> -   /*
> -    * Create the target relation by faking up a CREATE TABLE parsetree and
> -    * passing it to DefineRelation.
> -    */
> -   create->relation = into->rel;
> -   create->tableElts = attrList;
> -   create->inhRelations = NIL;
> -   create->ofTypename = NULL;
> -   create->constraints = NIL;
> -   create->options = into->options;
> -   create->oncommit = into->onCommit;
> -   create->tablespacename = into->tableSpaceName;
> -   create->if_not_exists = false;
> -   create->accessMethod = into->accessMethod;
> ```
> 
> It seems like there is very little shared in this function between the
> old code and new. Should replacing a matview just have a different
> function altogether?

I factored out the replace logic into create_ctas_replace leaving the
existing create_ctas_internal now untouched.  create_ctas_replace is
called from create_ctas_nodata where we check whether we're replacing an
existing matview.
 
> ```
> +   /* Check if an existing materialized view needs to be replaced. */
> +   if (is_matview)
> +   {
> +       LOCKMODE    lockmode;
> 
> -   /*
> -    * Create the relation.  (This will error out if there's an existing view,
> -    * so we don't need more code to complain if "replace" is false.)
> -    */
> -   intoRelationAddr = DefineRelation(create, relkind, InvalidOid, NULL, NULL);
> +       lockmode = into->replace ? AccessExclusiveLock : NoLock;
> +       (void) RangeVarGetAndCheckCreationNamespace(into->rel, lockmode,
> +                                                   &matviewOid);
> +       replace = OidIsValid(matviewOid) && into->replace;
> +   }
> ```
> 
> We didn't call RangeVarGetAndCheckCreationNamespace before, but now we
> call it whether you say `OR REPLACE` or not. That seems suspicious.
> Why do we need to call it even when you don't ask to replace an
> existing matview? We only use matviewOid if we're replacing, and we
> only lock the relation if we're replacing. So probably this should be
> `if (is_matview && into->replace)` (which makes this function seem
> even more strongly that it should be split into two).

RangeVarGetAndCheckCreationNamespace is now called in create_ctas_nodata
when is_matview && into->replace is true.

> ```
> +       rel = relation_open(matviewOid, NoLock);
> ```
> 
> The stanzas in this branch need comments. The code strongly resembles
> DefineVirtualRelation, but without the comments. For instance in this
> line, I wasn't sure why were opening the relation with no lock. This
> comment from DefineVirtualRelation is very helpful:
> 
> ```
> /* Relation is already locked, but we must build a relcache entry. */`
> ```
> 
> The other lost comments would be helpful too.

I added these comments now.

> ```
> +       /* add new attributes */
> +       if (list_length(attrList) > rel->rd_att->natts)
> +       {
> +           ListCell   *c;
> +           int         skip = rel->rd_att->natts;
> +
> +           foreach(c, attrList)
> +           {
> +               if (skip > 0)
> +               {
> +                   skip--;
> +                   continue;
> +               }
> +               atcmd = makeNode(AlterTableCmd);
> +               atcmd->subtype = AT_AddColumnToView;
> +               atcmd->def = (Node *) lfirst(c);
> +               atcmds = lappend(atcmds, atcmd);
> +           }
> +       }
> ```
> 
> Can we use list_nth_cell to start at the right position and avoid the
> manual skipping? (So the loop would be for instead of foreach.) Or
> even better, for_each_from looks perfect for this.

Now simplified with for_each_from.

> ```
> +       /* access method */
> +       atcmd = makeNode(AlterTableCmd);
> +       atcmd->subtype = AT_SetAccessMethod;
> +       atcmd->name = into->accessMethod ? into->accessMethod :
> default_table_access_method;
> +       atcmds = lappend(atcmds, atcmd);
> ```
> 
> Just to confirm, this is a noop if the access method is already the
> same? And likewise with the other AT subcommands?

It's a noop for access method and tablespace, but not for storage
options.

AT_SetAccessMethod: Checked by ATPrepSetAccessMethod to skip phase 3

AT_SetTableSpace: Checked by ATExecSetTableSpace using
CheckRelationTableSpaceMove in phase 3

AT_ReplaceRelOptions: Always replaces the current storage options
(ATExecSetRelOptions with transformRelOptions) in phase 2 regardless of
whether there are changes.

> ```
> @@ -234,7 +342,26 @@ ExecCreateTableAs(ParseState *pstate,
> CreateTableAsStmt *stmt,
> 
>     /* Check if the relation exists or not */
>     if (CreateTableAsRelExists(stmt))
> +   {
> +       /* An existing materialized view can be replaced. */
> +       if (is_matview && into->replace)
> +       {
> +           RefreshMatViewStmt *refresh;
> +
> +           /* Change the relation to match the new query and other options. */
> +           (void) create_ctas_nodata(query->targetList, into);
> +
> +           /* Refresh the materialized view with a fake statement. */
> +           refresh = makeNode(RefreshMatViewStmt);
> +           refresh->relation = into->rel;
> +           refresh->skipData = into->skipData;
> +           refresh->concurrent = false;
> +
> +           return ExecRefreshMatView(refresh, pstate->p_sourcetext, qc);
> +       }
> +
>         return InvalidObjectAddress;
> +   }
> ```
> 
> Maybe an `else` for the other case? Having parallel indentation
> communicates the parallel execution.

Done.

> ```
> @@ -402,14 +529,15 @@ CreateTableAsRelExists(CreateTableAsStmt *ctas)
>     oldrelid = get_relname_relid(into->rel->relname, nspid);
>     if (OidIsValid(oldrelid))
>     {
> -       if (!ctas->if_not_exists)
> +       if (!ctas->if_not_exists && !into->replace)
> ```
> 
> Changing the contract without changing the function comment seems wrong.
> 
> But is this really factored correctly? Maybe the check should happen
> in the caller instead.
> 
> Does this require a change to ExplainOneUtility, which also calls this
> function? At least we probably need to handle OR REPLACE there.

I added the static CreateTableAsRelReplaceable to leave the existing
CreateTableAsRelExists untouched.  The former now only checks whether
the relation exists and can be replaced.  The logic in ExecCreateTableAs
is changed to first check is_matview && into->replace.

> ```
>     if (newdesc->natts < olddesc->natts)
> -       ereport(ERROR,
> -               (errcode(ERRCODE_INVALID_TABLE_DEFINITION),
> -                errmsg("cannot drop columns from view")));
> +   {
> +       if (is_matview)
> +           ereport(ERROR,
> +                   errcode(ERRCODE_INVALID_TABLE_DEFINITION),
> +                   errmsg("cannot drop columns from materialized view"));
> +       else
> +           ereport(ERROR,
> +                   (errcode(ERRCODE_INVALID_TABLE_DEFINITION),
> +                    errmsg("cannot drop columns from view")));
> +   }
> ```
> 
> Instead of repeating so much, perhaps `errmsg("cannot drop columns
> from %s", is_matview ? "materialized view" : "view")`? Or since that
> is probably bad for translators, at least this:
> 
> ```
> errmsg(is_matview
>        ? "cannot drop columns from materialized view"
>        : "cannot drop columns from view")
> ```
> 
> Likewise with more error messages in this function. (There are a lot of them.)

When formatting it like errmsg(is_matview ? "" : "") the PO files only
pick up the first of the two messages.  Pulling out the ternary like
this would work:

	is_matview ? errmsg(...) : errmsg(...)

But instead I added function checkMatviewColumns to matview.c based on
checkViewColumns to avoid the is_matview flag altogether.  Both
functions still implement the same checks.  The very last comment in
checkViewColumns mentions leaving column defaults in place.  Since
default values can't be set on matview columns I figured that it's best
to provide these checks as separate functions to avoid confusion.

> The parser changes seem fine (see below re the third patch though).
> 
> Tab completion seems fine.
> 
> From v7-0002-Handle-default-tablespace-in-AlterTableInternal.patch:
> 
> These changes should get rolled into the previous patch. I think you
> broke them out to allow considering them separately, although your
> patches would be simpler if they were the first patch in the series
> then.

Yes, I thought separate patches would be easier to review.  v8 is now a
single patch.

> ```
> +       /* use empty string to specify default tablespace */
> +       atcmd->name = into->tableSpaceName ? into->tableSpaceName : "";
>         atcmds = lappend(atcmds, atcmd);
> ```
> 
> We didn't love using "" as a magic string to signal the
> AT_SetTableSpace subcommand. At least it should be a named constant,
> but maybe a separate struct member is better.

I changed the logic that has been factored out into create_ctas_replace
to now resolve the name of the default tablespace and store that in
atcmd->name.

> ```
> -   /* Check that the tablespace exists */
> -   tablespaceId = get_tablespace_oid(tablespacename, false);
> +   if (tablespacename != NULL && tablespacename[0] == '\0')
> +   {
> +       /* Use default tablespace if name is empty string */
> +       tablespaceId =
> GetDefaultTablespace(rel->rd_rel->relpersistence,
> rel->rd_rel->relispartition);
> +       if (!OidIsValid(tablespaceId))
> +           tablespaceId = MyDatabaseTableSpace;
> +   }
> +   else
> +   {
> +       /* Check that the tablespace exists */
> +       tablespaceId = get_tablespace_oid(tablespacename, false);
> +   }
> 
>     /* Check permissions except when moving to database's default */
>     if (OidIsValid(tablespaceId) && tablespaceId != MyDatabaseTableSpace)
> ```
> 
> This seems like something that should happen during analysis, but I
> know we are less strict for utility statements.
> 
> Are there any locking issues with calling GetDefaultTablespace here
> instead of earlier? Why does ATPrepSetTableSpace take a lockmode but
> not use it? Can the table space get dropped in the middle of the
> command? But this is probably handled already. Do we need the lock the
> *old* tablespace?

In case of ALTER TABLE SET TABLESPACE the old and new tablespaces aren't
locked either as far as I can see.  Maybe I'm missing something in
tablecmds.c because I would expect something similar to
RangeVarGetAndCheckCreationNamespace taking an AccessShareLock on the
namespace.

ATPrepSetTableSpace takes the lockmode parameter since 2dbbda02e7e which
added lockmode to other ATPrepXXX functions as well without using it.  I
guess this was done to provide a consistent API across the various ALTER
TABLE subcommands.

> What about partitions?

Table partitions do not apply to matviews.
 
> From v7-0003-Add-WITH-OLD-DATA-to-CREATE-OR-REPLACE-MATERIALIZ.patch:
> 
> ```
> @@ -383,6 +391,9 @@ ExecCreateTableAs(ParseState *pstate,
> CreateTableAsStmt *stmt,
>      */
>     if (is_matview)
>     {
> +       if (into->keepData)
> +           elog(ERROR, "must not specify WITH OLD DATA when creating
> a new materialized view");
> +
>         do_refresh = !into->skipData;
>         into->skipData = true;
>     }
> ```
> 
> Should this be ereport? I guess the current parser productions should
> prevent us from reaching this code though. So perhaps a comment?

Right.  Replaced with ereport and a localizable error message.

> ```
> diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
> index 7aaf0e37ad8..e51ce2701be 100644
> --- a/src/backend/parser/gram.y
> +++ b/src/backend/parser/gram.y
> @@ -4944,6 +4944,22 @@ CreateMatViewStmt:
>                     $7->replace = true;
>                     $$ = (Node *) ctas;
>                 }
> +       | CREATE OR REPLACE OptNoLog MATERIALIZED VIEW
> create_mv_target AS SelectStmt WITH OLD DATA_P
> +               {
> +                   CreateTableAsStmt *ctas = makeNode(CreateTableAsStmt);
> +
> +                   ctas->query = $9;
> +                   ctas->into = $7;
> +                   ctas->objtype = OBJECT_MATVIEW;
> +                   ctas->is_select_into = false;
> +                   ctas->if_not_exists = false;
> +                   /* cram additional flags into the IntoClause */
> +                   $7->rel->relpersistence = $4;
> +                   $7->skipData = false;
> +                   $7->keepData = true;
> +                   $7->replace = true;
> +                   $$ = (Node *) ctas;
> +               }
>         ;
> ```
> 
> This is the fourth production for minor variations of
> CreateMatViewStmt, which seems like a lot. Perhaps instead of
> opt_with_data we add a new production opt_with_no_or_old_data that can
> return 3 alteratives (WITH [{NO|OLD}] DATA)?

I've added production opt_with_no_or_old_data as suggested, backed by
the new enum WithDataOption in parsenodes.h.  With that, I replaced
.keepData from v7 with just .data holding that new enum.

Field .skipData is still in place but I think it should be migrated to
that new .data field.  I haven't done that in v8, though, to keep the
patch focused.

> ```
> +-- replace query but keep old data
> +CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
> +  SELECT 5 AS a
> +  WITH OLD DATA;
> +SELECT * FROM mvtest_replace;
> +REFRESH MATERIALIZED VIEW mvtest_replace;
> +SELECT * FROM mvtest_replace;
> +
> ```
> 
> We would like to see a test adding a new column while using WITH OLD
> DATA. It is safe because every heap tuple knows how many attributes it
> holds (and I tested it), but I did wonder if anyone had been tempted
> to call fastgetattr directly for matviews. (Before your feature, it
> would have been safe, I think.) Here is the test I did:
> 
> ```
> create materialized view mvnulls as
>   select 1 c1, 2 c2, 3 c3, 4 c4, 5 c5, 6 c6, 7 c7, null c8;
> -- add a ninth column (exceeding the old t_bits):
> create or replace materialized view mvnulls as
>   select 1 c1, 2 c2, 3 c3, 4 c4, 5 c5, 6 c6, 7 c7, null c8, null c9
> with old data;
> select c9 from mvnulls;
> ```

That test case is now included.

> Another thing to think about and test: what if while using WITH OLD
> DATA the new matview definition violates a unique index? Or a domain?
> Are there any other constraints that can be applied to matviews to
> consider?

Adding a column of a domain type with NOT NULL constraint can indeed
cause WITH OLD DATA to fail since we're setting new columns to NULL in
that case.  I've added the following test case:

	CREATE DOMAIN mvtest_dom AS int
	  CONSTRAINT mvtest_dom_nn NOT NULL;
	CREATE MATERIALIZED VIEW mvtest_replace AS
	  SELECT 1::mvtest_dom AS a;
	CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
	  SELECT 2::mvtest_dom AS a, 3::mvtest_dom AS b
	  WITH OLD DATA;

But besides that I don't see how keeping the old data could violate a
unique index or any other constraint on a domain type.  During the
execution of the matview replacement the unique index and domain types
remain unaltered.  So the old data will still satisfy any pre-existing
constraints.

And changing the type of an existing column is not possible.  So you
can't switch to a different domain type with more restrictive
constraints while keeping the old data.  For example, this won't work
since column "a" can't be changed from mvtest_dom1 to mvtest_dom2 on
principle:

	CREATE DOMAIN mvtest_dom1 AS int
	  CONSTRAINT mvtest_dom1_check CHECK (VALUE = 1);
	CREATE DOMAIN mvtest_dom2 AS int
	  CONSTRAINT mvtest_dom2_check CHECK (VALUE = 2);
	CREATE MATERIALIZED VIEW mvtest_replace AS
	  SELECT 1::mvtest_dom1 AS a;
	CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
	  SELECT 2::mvtest_dom2 AS a
	  WITH OLD DATA;

-- 
Erik Wienhold
From dcf65b62993f5ec831d670761e37004e9eec8e94 Mon Sep 17 00:00:00 2001
From: Erik Wienhold <ewie@ewie.name>
Date: Tue, 21 May 2024 18:35:47 +0200
Subject: [PATCH v8] Add OR REPLACE option to CREATE MATERIALIZED VIEW

Add WITH OLD DATA to keep the current data when replacing a materialized
view which is useful when running REFRESH MATERIALIZED VIEW CONCURRENTLY
afterwards.
---
 .../sgml/ref/create_materialized_view.sgml    |  30 +-
 src/backend/commands/createas.c               | 229 +++++++++++++++-
 src/backend/commands/matview.c                |  80 ++++++
 src/backend/commands/tablecmds.c              |  11 +-
 src/backend/parser/gram.y                     |  25 ++
 src/bin/psql/tab-complete.in.c                |  38 ++-
 src/include/commands/matview.h                |   2 +
 src/include/nodes/parsenodes.h                |   4 +-
 src/include/nodes/primnodes.h                 |  10 +
 src/test/regress/expected/matview.out         | 258 ++++++++++++++++++
 src/test/regress/sql/matview.sql              | 152 +++++++++++
 src/tools/pgindent/typedefs.list              |   1 +
 12 files changed, 813 insertions(+), 27 deletions(-)

diff --git a/doc/src/sgml/ref/create_materialized_view.sgml b/doc/src/sgml/ref/create_materialized_view.sgml
index 62d897931c3..e6c7b7c7d37 100644
--- a/doc/src/sgml/ref/create_materialized_view.sgml
+++ b/doc/src/sgml/ref/create_materialized_view.sgml
@@ -21,13 +21,13 @@ PostgreSQL documentation
 
  <refsynopsisdiv>
 <synopsis>
-CREATE MATERIALIZED VIEW [ IF NOT EXISTS ] <replaceable>table_name</replaceable>
+CREATE [ OR REPLACE ] MATERIALIZED VIEW [ IF NOT EXISTS ] <replaceable>table_name</replaceable>
     [ (<replaceable>column_name</replaceable> [, ...] ) ]
     [ USING <replaceable class="parameter">method</replaceable> ]
     [ WITH ( <replaceable class="parameter">storage_parameter</replaceable> [= <replaceable class="parameter">value</replaceable>] [, ... ] ) ]
     [ TABLESPACE <replaceable class="parameter">tablespace_name</replaceable> ]
     AS <replaceable>query</replaceable>
-    [ WITH [ NO ] DATA ]
+    [ WITH [ NO | OLD ] DATA ]
 </synopsis>
  </refsynopsisdiv>
 
@@ -37,7 +37,8 @@ CREATE MATERIALIZED VIEW [ IF NOT EXISTS ] <replaceable>table_name</replaceable>
   <para>
    <command>CREATE MATERIALIZED VIEW</command> defines a materialized view of
    a query.  The query is executed and used to populate the view at the time
-   the command is issued (unless <command>WITH NO DATA</command> is used) and may be
+   the command is issued (unless <command>WITH NO DATA</command> or
+   <command>WITH OLD DATA</command> is used) and may be
    refreshed later using <command>REFRESH MATERIALIZED VIEW</command>.
   </para>
 
@@ -60,6 +61,17 @@ CREATE MATERIALIZED VIEW [ IF NOT EXISTS ] <replaceable>table_name</replaceable>
   <title>Parameters</title>
 
   <variablelist>
+   <varlistentry>
+    <term><literal>OR REPLACE</literal></term>
+    <listitem>
+     <para>
+      Replaces a materialized view if it already exists.
+      Specifying <literal>OR REPLACE</literal> together with
+      <literal>IF NOT EXISTS</literal> is an error.
+     </para>
+    </listitem>
+   </varlistentry>
+
    <varlistentry>
     <term><literal>IF NOT EXISTS</literal></term>
     <listitem>
@@ -67,7 +79,7 @@ CREATE MATERIALIZED VIEW [ IF NOT EXISTS ] <replaceable>table_name</replaceable>
       Do not throw an error if a materialized view with the same name already
       exists. A notice is issued in this case.  Note that there is no guarantee
       that the existing materialized view is anything like the one that would
-      have been created.
+      have been created, unlike when using <literal>OR REPLACE</literal>.
      </para>
     </listitem>
    </varlistentry>
@@ -151,7 +163,7 @@ CREATE MATERIALIZED VIEW [ IF NOT EXISTS ] <replaceable>table_name</replaceable>
    </varlistentry>
 
    <varlistentry>
-    <term><literal>WITH [ NO ] DATA</literal></term>
+    <term><literal>WITH [ NO | OLD ] DATA</literal></term>
     <listitem>
      <para>
       This clause specifies whether or not the materialized view should be
@@ -159,6 +171,14 @@ CREATE MATERIALIZED VIEW [ IF NOT EXISTS ] <replaceable>table_name</replaceable>
       flagged as unscannable and cannot be queried until <command>REFRESH
       MATERIALIZED VIEW</command> is used.
      </para>
+
+     <para>
+      The form <command>WITH OLD DATA</command> keeps the already stored data
+      when replacing an existing materialized view to keep it populated.  Use
+      this form if you want to use <command>REFRESH MATERIALIZED VIEW CONCURRENTLY</command>
+      as it requires a populated materialized view.  It is an error to use this
+      form when creating a new materialized view.
+     </para>
     </listitem>
    </varlistentry>
 
diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c
index 6dbb831ca89..cbc384dd3f7 100644
--- a/src/backend/commands/createas.c
+++ b/src/backend/commands/createas.c
@@ -34,9 +34,11 @@
 #include "commands/matview.h"
 #include "commands/prepare.h"
 #include "commands/tablecmds.h"
+#include "commands/tablespace.h"
 #include "commands/view.h"
 #include "executor/execdesc.h"
 #include "executor/executor.h"
+#include "miscadmin.h"
 #include "nodes/makefuncs.h"
 #include "nodes/nodeFuncs.h"
 #include "nodes/queryjumble.h"
@@ -63,6 +65,7 @@ typedef struct
 /* utility functions for CTAS definition creation */
 static ObjectAddress create_ctas_internal(List *attrList, IntoClause *into);
 static ObjectAddress create_ctas_nodata(List *tlist, IntoClause *into);
+static ObjectAddress create_ctas_replace(List *tlist, IntoClause *into, Oid matviewOid);
 
 /* DestReceiver routines for collecting data */
 static void intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo);
@@ -70,6 +73,7 @@ static bool intorel_receive(TupleTableSlot *slot, DestReceiver *self);
 static void intorel_shutdown(DestReceiver *self);
 static void intorel_destroy(DestReceiver *self);
 
+static bool CreateTableAsRelReplaceable(CreateTableAsStmt *ctas);
 
 /*
  * create_ctas_internal
@@ -157,6 +161,8 @@ create_ctas_nodata(List *tlist, IntoClause *into)
 	List	   *attrList;
 	ListCell   *t,
 			   *lc;
+	bool		is_matview = (into->viewQuery != NULL);
+	Oid			matviewOid = InvalidOid;
 
 	/*
 	 * Build list of ColumnDefs from non-junk elements of the tlist.  If a
@@ -211,8 +217,146 @@ create_ctas_nodata(List *tlist, IntoClause *into)
 				(errcode(ERRCODE_SYNTAX_ERROR),
 				 errmsg("too many column names were specified")));
 
-	/* Create the relation definition using the ColumnDef list */
-	return create_ctas_internal(attrList, into);
+	/* Get the existing matview to be replaced */
+	if (is_matview && into->replace)
+		(void) RangeVarGetAndCheckCreationNamespace(into->rel,
+													AccessExclusiveLock,
+													&matviewOid);
+
+	if (OidIsValid(matviewOid))
+		/* Replace the existing matview */
+		return create_ctas_replace(attrList, into, matviewOid);
+	else
+		/* Create the relation definition using the ColumnDef list */
+		return create_ctas_internal(attrList, into);
+}
+
+
+/*
+ * create_ctas_replace
+ *
+ * Internal utility used for replacing the definition of a materialized view.
+ * Caller needs to provide a list of attributes (ColumnDef nodes) and the
+ * materialized view OID.
+ */
+static ObjectAddress
+create_ctas_replace(List *attrList, IntoClause *into, Oid matviewOid)
+{
+	ObjectAddress intoRelationAddr;
+	Relation	rel;
+	List	   *atcmds = NIL;
+	AlterTableCmd *atcmd;
+	TupleDesc	descriptor;
+	Query	   *query;
+
+	/* Relation is already locked, but we must build a relcache entry. */
+	rel = relation_open(matviewOid, NoLock);
+
+	/* Make sure it *is* a matview. */
+	if (rel->rd_rel->relkind != RELKIND_MATVIEW)
+		ereport(ERROR,
+				errcode(ERRCODE_WRONG_OBJECT_TYPE),
+				errmsg("\"%s\" is not a materialized view",
+					   RelationGetRelationName(rel)));
+
+	/* Also check it's not in use already */
+	CheckTableNotInUse(rel, "CREATE OR REPLACE MATERIALIZED VIEW");
+
+	descriptor = BuildDescForRelation(attrList);
+	checkMatviewColumns(descriptor, rel->rd_att);
+
+	/*
+	 * If new attributes have been added, we must add pg_attribute entries for
+	 * them.  It is convenient (although overkill) to use the ALTER TABLE ADD
+	 * COLUMN infrastructure for this.
+	 *
+	 * Note that we must do this before updating the query for the matview,
+	 * since the rules system requires that the correct matview columns be in
+	 * place when defining the new rules.
+	 *
+	 * Also note that ALTER TABLE doesn't run parse transformation on
+	 * AT_AddColumnToView commands.  The ColumnDef we supply must be ready to
+	 * execute as-is.
+	 */
+	if (list_length(attrList) > rel->rd_att->natts)
+	{
+		ListCell   *c;
+
+		for_each_from(c, attrList, rel->rd_att->natts)
+		{
+			atcmd = makeNode(AlterTableCmd);
+			atcmd->subtype = AT_AddColumnToView;
+			atcmd->def = (Node *) lfirst(c);
+			atcmds = lappend(atcmds, atcmd);
+		}
+	}
+
+	/*
+	 * Use ALTER TABLE to set access method, tablespace, and storage options.
+	 * When replacing an existing matview we need to alter the relation such
+	 * that the defaults apply as if they have not been specified at all by
+	 * the CREATE OR REPLACE MATERIALIZED VIEW statement.
+	 */
+
+	/* access method */
+	atcmd = makeNode(AlterTableCmd);
+	atcmd->subtype = AT_SetAccessMethod;
+	atcmd->name = into->accessMethod
+		? into->accessMethod : default_table_access_method;
+	atcmds = lappend(atcmds, atcmd);
+
+	/* tablespace */
+	atcmd = makeNode(AlterTableCmd);
+	atcmd->subtype = AT_SetTableSpace;
+	if (into->tableSpaceName)
+		atcmd->name = into->tableSpaceName;
+	else
+	{
+		Oid			spcOid;
+		char	   *spcName;
+
+		/*
+		 * Must use the default tablespace if no explicit tablespace is
+		 * specified.
+		 */
+		spcOid = GetDefaultTablespace(RELPERSISTENCE_PERMANENT, false);
+		if (!OidIsValid(spcOid))
+			spcOid = MyDatabaseTableSpace;
+
+		spcName = get_tablespace_name(spcOid);
+		if (!spcName)			/* should not happen */
+			elog(ERROR, "could not find tuple for tablespace %u", spcOid);
+
+		atcmd->name = spcName;
+	}
+	atcmds = lappend(atcmds, atcmd);
+
+	/* storage options */
+	atcmd = makeNode(AlterTableCmd);
+	atcmd->subtype = AT_ReplaceRelOptions;
+	atcmd->def = (Node *) into->options;
+	atcmds = lappend(atcmds, atcmd);
+
+	/* EventTriggerAlterTableStart called by ProcessUtilitySlow */
+	AlterTableInternal(matviewOid, atcmds, true);
+
+	/* Make the new matview columns visible */
+	CommandCounterIncrement();
+
+	relation_close(rel, NoLock);
+	ObjectAddressSet(intoRelationAddr, RelationRelationId, matviewOid);
+
+	/*
+	 * Replace the "view" part of the matview.  StoreViewQuery scribbles on
+	 * tree, so make a copy.
+	 */
+	query = copyObject(into->viewQuery);
+	StoreViewQuery(intoRelationAddr.objectId, query, true);
+
+	/* Make the new matview query visible */
+	CommandCounterIncrement();
+
+	return intoRelationAddr;
 }
 
 
@@ -232,8 +376,37 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt,
 	DestReceiver *dest;
 	ObjectAddress address;
 
-	/* Check if the relation exists or not */
-	if (CreateTableAsRelExists(stmt))
+	/*
+	 * Check if the relation exists or not.  An existing materialized view can
+	 * be replaced.
+	 */
+	if (is_matview && into->replace)
+	{
+		if (CreateTableAsRelReplaceable(stmt))
+		{
+			/* Change the relation to match the new query and other options. */
+			address = create_ctas_nodata(query->targetList, into);
+
+			/*
+			 * Refresh the materialized view with a fake statement unless we
+			 * must keep the old data.
+			 */
+			if (into->data != WITHDATA_OLD)
+			{
+				RefreshMatViewStmt *refresh;
+
+				refresh = makeNode(RefreshMatViewStmt);
+				refresh->relation = into->rel;
+				refresh->skipData = into->skipData;
+				refresh->concurrent = false;
+
+				address = ExecRefreshMatView(refresh, pstate->p_sourcetext, qc);
+			}
+
+			return address;
+		}
+	}
+	else if (CreateTableAsRelExists(stmt))
 		return InvalidObjectAddress;
 
 	/*
@@ -273,6 +446,11 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt,
 	 */
 	if (is_matview)
 	{
+		if (into->data == WITHDATA_OLD)
+			ereport(ERROR,
+					(errcode(ERRCODE_SYNTAX_ERROR),
+					 errmsg("must not specify WITH OLD DATA when creating a new materialized view")));
+
 		do_refresh = !into->skipData;
 		into->skipData = true;
 	}
@@ -429,6 +607,49 @@ CreateTableAsRelExists(CreateTableAsStmt *ctas)
 	return false;
 }
 
+/*
+ * CreateTableAsRelReplaceable --- check existence of replaceable relation for
+ * CreateTableAsStmt
+ *
+ * Utility wrapper checking if the relation pending for creation in this
+ * CreateTableAsStmt query already exists or not.  Returns true if the relation
+ * exists and should be replaced, otherwise false.
+ */
+static bool
+CreateTableAsRelReplaceable(CreateTableAsStmt *ctas)
+{
+	Oid			nspid;
+	Oid			oldrelid;
+	ObjectAddress address;
+	IntoClause *into = ctas->into;
+
+	nspid = RangeVarGetCreationNamespace(into->rel);
+
+	oldrelid = get_relname_relid(into->rel->relname, nspid);
+	if (OidIsValid(oldrelid))
+	{
+		if (!into->replace)
+			ereport(ERROR,
+					(errcode(ERRCODE_DUPLICATE_TABLE),
+					 errmsg("relation \"%s\" already exists",
+							into->rel->relname)));
+
+		/*
+		 * The relation exists and OR REPLACE has been specified.
+		 *
+		 * If we are in an extension script, insist that the pre-existing
+		 * object be a member of the extension, to avoid security risks.
+		 */
+		ObjectAddressSet(address, RelationRelationId, oldrelid);
+		checkMembershipInCurrentExtension(&address);
+
+		return true;
+	}
+
+	/* Relation does not exist, it can be created */
+	return false;
+}
+
 /*
  * CreateIntoRelDestReceiver -- create a suitable DestReceiver object
  *
diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c
index f7d8007f796..853eae37057 100644
--- a/src/backend/commands/matview.c
+++ b/src/backend/commands/matview.c
@@ -967,3 +967,83 @@ CloseMatViewIncrementalMaintenance(void)
 	matview_maintenance_depth--;
 	Assert(matview_maintenance_depth >= 0);
 }
+
+/*
+ * Verify that the columns associated with proposed new matview definition
+ * match the columns of the old matview.  This is similar to equalRowTypes(),
+ * with code added to generate specific complaints.  Also, we allow the new
+ * matview to have more columns than the old.
+ */
+void
+checkMatviewColumns(TupleDesc newdesc, TupleDesc olddesc)
+{
+	int			i;
+
+	if (newdesc->natts < olddesc->natts)
+	{
+		ereport(ERROR,
+				errcode(ERRCODE_INVALID_TABLE_DEFINITION),
+				errmsg("cannot drop columns from materialized view"));
+	}
+
+	for (i = 0; i < olddesc->natts; i++)
+	{
+		Form_pg_attribute newattr = TupleDescAttr(newdesc, i);
+		Form_pg_attribute oldattr = TupleDescAttr(olddesc, i);
+
+		/* XXX msg not right, but we don't support DROP COL on matview anyway */
+		if (newattr->attisdropped != oldattr->attisdropped)
+		{
+			ereport(ERROR,
+					errcode(ERRCODE_INVALID_TABLE_DEFINITION),
+					errmsg("cannot drop columns from materialized view"));
+		}
+
+		if (strcmp(NameStr(newattr->attname), NameStr(oldattr->attname)) != 0)
+		{
+			ereport(ERROR,
+					errcode(ERRCODE_INVALID_TABLE_DEFINITION),
+					errmsg("cannot change name of materialized view column \"%s\" to \"%s\"",
+						   NameStr(oldattr->attname),
+						   NameStr(newattr->attname)),
+					errhint("Use ALTER MATERIALIZED VIEW ... RENAME COLUMN ... to change name of materialized view column instead."));
+		}
+
+		/*
+		 * We cannot allow type, typmod, or collation to change, since these
+		 * properties may be embedded in Vars of other views/rules referencing
+		 * this one.  Other column attributes can be ignored.
+		 */
+		if (newattr->atttypid != oldattr->atttypid ||
+			newattr->atttypmod != oldattr->atttypmod)
+		{
+			ereport(ERROR,
+					errcode(ERRCODE_INVALID_TABLE_DEFINITION),
+					errmsg("cannot change data type of materialized view column \"%s\" from %s to %s",
+						   NameStr(oldattr->attname),
+						   format_type_with_typemod(oldattr->atttypid,
+													oldattr->atttypmod),
+						   format_type_with_typemod(newattr->atttypid,
+													newattr->atttypmod)));
+		}
+
+		/*
+		 * At this point, attcollations should be both valid or both invalid,
+		 * so applying get_collation_name unconditionally should be fine.
+		 */
+		if (newattr->attcollation != oldattr->attcollation)
+		{
+			ereport(ERROR,
+					errcode(ERRCODE_INVALID_TABLE_DEFINITION),
+					errmsg("cannot change collation of materialized view column \"%s\" from \"%s\" to \"%s\"",
+						   NameStr(oldattr->attname),
+						   get_collation_name(oldattr->attcollation),
+						   get_collation_name(newattr->attcollation)));
+		}
+	}
+
+	/*
+	 * We ignore the constraint fields since the new matview desc can't have
+	 * any constraints.
+	 */
+}
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 472db112fa7..5bf322257c5 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -4720,7 +4720,8 @@ AlterTableGetLockLevel(List *cmds)
 				 * Subcommands that may be visible to concurrent SELECTs
 				 */
 			case AT_DropColumn: /* change visible to SELECT */
-			case AT_AddColumnToView:	/* CREATE VIEW */
+			case AT_AddColumnToView:	/* via CREATE OR REPLACE
+										 * [MATERIALIZED] VIEW */
 			case AT_DropOids:	/* used to equiv to DropColumn */
 			case AT_EnableAlwaysRule:	/* may change SELECT rules */
 			case AT_EnableReplicaRule:	/* may change SELECT rules */
@@ -5020,8 +5021,9 @@ ATPrepCmd(List **wqueue, Relation rel, AlterTableCmd *cmd,
 			/* Recursion occurs during execution phase */
 			pass = AT_PASS_ADD_COL;
 			break;
-		case AT_AddColumnToView:	/* add column via CREATE OR REPLACE VIEW */
-			ATSimplePermissions(cmd->subtype, rel, ATT_VIEW);
+		case AT_AddColumnToView:	/* via CREATE OR REPLACE [MATERIALIZED]
+									 * VIEW */
+			ATSimplePermissions(cmd->subtype, rel, ATT_VIEW | ATT_MATVIEW);
 			ATPrepAddColumn(wqueue, rel, recurse, recursing, true, cmd,
 							lockmode, context);
 			/* Recursion occurs during execution phase */
@@ -5458,7 +5460,8 @@ ATExecCmd(List **wqueue, AlteredTableInfo *tab,
 	switch (cmd->subtype)
 	{
 		case AT_AddColumn:		/* ADD COLUMN */
-		case AT_AddColumnToView:	/* add column via CREATE OR REPLACE VIEW */
+		case AT_AddColumnToView:	/* via CREATE OR REPLACE [MATERIALIZED]
+									 * VIEW */
 			address = ATExecAddColumn(wqueue, tab, rel, &cmd,
 									  cmd->recurse, false,
 									  lockmode, cur_pass, context);
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index ff4e1388c55..91e52689d7a 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -250,6 +250,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 	Alias	   *alias;
 	RangeVar   *range;
 	IntoClause *into;
+	WithDataOption withdata;
 	WithClause *with;
 	InferClause	*infer;
 	OnConflictClause *onconflict;
@@ -358,6 +359,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 				opt_grant_grant_option
 				opt_nowait opt_if_exists opt_with_data
 				opt_transaction_chain
+%type <withdata> opt_with_no_or_old_data
 %type <list>	grant_role_opt_list
 %type <defelt>	grant_role_opt
 %type <node>	grant_role_opt_value
@@ -5030,6 +5032,13 @@ opt_with_data:
 			| /*EMPTY*/								{ $$ = true; }
 		;
 
+opt_with_no_or_old_data:
+			WITH DATA_P								{ $$ = WITHDATA_DEFAULT; }
+			| WITH NO DATA_P						{ $$ = WITHDATA_NONE; }
+			| WITH OLD DATA_P						{ $$ = WITHDATA_OLD; }
+			| /*EMPTY*/								{ $$ = WITHDATA_DEFAULT; }
+		;
+
 
 /*****************************************************************************
  *
@@ -5067,6 +5076,22 @@ CreateMatViewStmt:
 					$8->skipData = !($11);
 					$$ = (Node *) ctas;
 				}
+		| CREATE OR REPLACE OptNoLog MATERIALIZED VIEW create_mv_target AS SelectStmt opt_with_no_or_old_data
+				{
+					CreateTableAsStmt *ctas = makeNode(CreateTableAsStmt);
+
+					ctas->query = $9;
+					ctas->into = $7;
+					ctas->objtype = OBJECT_MATVIEW;
+					ctas->is_select_into = false;
+					ctas->if_not_exists = false;
+					/* cram additional flags into the IntoClause */
+					$7->rel->relpersistence = $4;
+					$7->skipData = $10 == WITHDATA_NONE;
+					$7->data = $10;
+					$7->replace = true;
+					$$ = (Node *) ctas;
+				}
 		;
 
 create_mv_target:
diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c
index 46b9add0604..e683e4b1b33 100644
--- a/src/bin/psql/tab-complete.in.c
+++ b/src/bin/psql/tab-complete.in.c
@@ -2225,7 +2225,7 @@ match_previous_words(int pattern_id,
 	/* complete with something you can create or replace */
 	else if (TailMatches("CREATE", "OR", "REPLACE"))
 		COMPLETE_WITH("FUNCTION", "PROCEDURE", "LANGUAGE", "RULE", "VIEW",
-					  "AGGREGATE", "TRANSFORM", "TRIGGER");
+					  "AGGREGATE", "TRANSFORM", "TRIGGER", "MATERIALIZED VIEW");
 
 /* DROP, but not DROP embedded in other commands */
 	/* complete with something you can drop */
@@ -4256,28 +4256,42 @@ match_previous_words(int pattern_id,
 		COMPLETE_WITH("SELECT");
 
 /* CREATE MATERIALIZED VIEW */
-	else if (Matches("CREATE", "MATERIALIZED"))
+	else if (Matches("CREATE", "MATERIALIZED") ||
+			 Matches("CREATE", "OR", "REPLACE", "MATERIALIZED"))
 		COMPLETE_WITH("VIEW");
-	/* Complete CREATE MATERIALIZED VIEW <name> with AS or USING */
-	else if (Matches("CREATE", "MATERIALIZED", "VIEW", MatchAny))
+
+	/*
+	 * Complete CREATE [ OR REPLACE ] MATERIALIZED VIEW <name> with AS or
+	 * USING
+	 */
+	else if (Matches("CREATE", "MATERIALIZED", "VIEW", MatchAny) ||
+			 Matches("CREATE", "OR", "REPLACE", "MATERIALIZED", "VIEW", MatchAny))
 		COMPLETE_WITH("AS", "USING");
 
 	/*
-	 * Complete CREATE MATERIALIZED VIEW <name> USING with list of access
-	 * methods
+	 * Complete CREATE [ OR REPLACE ] MATERIALIZED VIEW <name> USING with list
+	 * of access methods
 	 */
-	else if (Matches("CREATE", "MATERIALIZED", "VIEW", MatchAny, "USING"))
+	else if (Matches("CREATE", "MATERIALIZED", "VIEW", MatchAny, "USING") ||
+			 Matches("CREATE", "OR", "REPLACE", "MATERIALIZED", "VIEW", MatchAny, "USING"))
 		COMPLETE_WITH_QUERY(Query_for_list_of_table_access_methods);
-	/* Complete CREATE MATERIALIZED VIEW <name> USING <access method> with AS */
-	else if (Matches("CREATE", "MATERIALIZED", "VIEW", MatchAny, "USING", MatchAny))
+
+	/*
+	 * Complete CREATE [ OR REPLACE ] MATERIALIZED VIEW <name> USING <access
+	 * method> with AS
+	 */
+	else if (Matches("CREATE", "MATERIALIZED", "VIEW", MatchAny, "USING", MatchAny) ||
+			 Matches("CREATE", "OR", "REPLACE", "MATERIALIZED", "VIEW", MatchAny, "USING", MatchAny))
 		COMPLETE_WITH("AS");
 
 	/*
-	 * Complete CREATE MATERIALIZED VIEW <name> [USING <access method> ] AS
-	 * with "SELECT"
+	 * Complete CREATE [ OR REPLACE ] MATERIALIZED VIEW <name> [USING <access
+	 * method> ] AS with "SELECT"
 	 */
 	else if (Matches("CREATE", "MATERIALIZED", "VIEW", MatchAny, "AS") ||
-			 Matches("CREATE", "MATERIALIZED", "VIEW", MatchAny, "USING", MatchAny, "AS"))
+			 Matches("CREATE", "OR", "REPLACE", "MATERIALIZED", "VIEW", MatchAny, "AS") ||
+			 Matches("CREATE", "MATERIALIZED", "VIEW", MatchAny, "USING", MatchAny, "AS") ||
+			 Matches("CREATE", "OR", "REPLACE", "MATERIALIZED", "VIEW", MatchAny, "USING", MatchAny, "AS"))
 		COMPLETE_WITH("SELECT");
 
 /* CREATE EVENT TRIGGER */
diff --git a/src/include/commands/matview.h b/src/include/commands/matview.h
index 738c731c1a9..140f7e96696 100644
--- a/src/include/commands/matview.h
+++ b/src/include/commands/matview.h
@@ -33,4 +33,6 @@ extern DestReceiver *CreateTransientRelDestReceiver(Oid transientoid);
 
 extern bool MatViewIncrementalMaintenanceIsEnabled(void);
 
+extern void checkMatviewColumns(TupleDesc newdesc, TupleDesc olddesc);
+
 #endif							/* MATVIEW_H */
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 4133c404a6b..7c629d09fb1 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -2519,7 +2519,8 @@ typedef struct AlterTableStmt
 typedef enum AlterTableType
 {
 	AT_AddColumn,				/* add column */
-	AT_AddColumnToView,			/* implicitly via CREATE OR REPLACE VIEW */
+	AT_AddColumnToView,			/* implicitly via CREATE OR REPLACE
+								 * [MATERIALIZED] VIEW */
 	AT_ColumnDefault,			/* alter column default */
 	AT_CookedColumnDefault,		/* add a pre-cooked column default */
 	AT_DropNotNull,				/* alter column drop not null */
@@ -4595,5 +4596,4 @@ typedef struct WaitStmt
 	List	   *options;		/* List of DefElem nodes */
 } WaitStmt;
 
-
 #endif							/* PARSENODES_H */
diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h
index bb05aeebee4..3b916993dbe 100644
--- a/src/include/nodes/primnodes.h
+++ b/src/include/nodes/primnodes.h
@@ -147,6 +147,14 @@ typedef struct TableFunc
 	ParseLoc	location;
 } TableFunc;
 
+/* WITH DATA option of CREATE MATERIALIZED VIEW */
+typedef enum WithDataOption
+{
+	WITHDATA_DEFAULT,			/* WITH DATA */
+	WITHDATA_NONE,				/* WITH NO DATA */
+	WITHDATA_OLD,				/* WITH OLD DATA */
+} WithDataOption;
+
 /*
  * IntoClause - target information for SELECT INTO, CREATE TABLE AS, and
  * CREATE MATERIALIZED VIEW
@@ -170,6 +178,8 @@ typedef struct IntoClause
 	/* materialized view's SELECT query */
 	struct Query *viewQuery pg_node_attr(query_jumble_ignore);
 	bool		skipData;		/* true for WITH NO DATA */
+	WithDataOption data;		/* WITH [ NO | OLD ] DATA */
+	bool		replace;		/* replace existing matview? */
 } IntoClause;
 
 
diff --git a/src/test/regress/expected/matview.out b/src/test/regress/expected/matview.out
index 0355720dfc6..e923aa4f0a7 100644
--- a/src/test/regress/expected/matview.out
+++ b/src/test/regress/expected/matview.out
@@ -699,3 +699,261 @@ NOTICE:  relation "matview_ine_tab" already exists, skipping
 (0 rows)
 
 DROP MATERIALIZED VIEW matview_ine_tab;
+--
+-- Test CREATE OR REPLACE MATERIALIZED VIEW
+--
+-- Matview does not already exist
+DROP MATERIALIZED VIEW IF EXISTS mvtest_replace;
+NOTICE:  materialized view "mvtest_replace" does not exist, skipping
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 1 AS a;
+SELECT * FROM mvtest_replace;
+ a 
+---
+ 1
+(1 row)
+
+-- Replace query with data
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 2 AS a;
+SELECT * FROM mvtest_replace;
+ a 
+---
+ 2
+(1 row)
+
+-- Replace query without data
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 3 AS a
+  WITH NO DATA;
+SELECT * FROM mvtest_replace; -- error: not populated
+ERROR:  materialized view "mvtest_replace" has not been populated
+HINT:  Use the REFRESH MATERIALIZED VIEW command.
+REFRESH MATERIALIZED VIEW mvtest_replace;
+SELECT * FROM mvtest_replace;
+ a 
+---
+ 3
+(1 row)
+
+-- Replace query but keep old data
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 5 AS a
+  WITH OLD DATA;
+SELECT * FROM mvtest_replace;
+ a 
+---
+ 3
+(1 row)
+
+REFRESH MATERIALIZED VIEW mvtest_replace;
+SELECT * FROM mvtest_replace;
+ a 
+---
+ 5
+(1 row)
+
+-- Add column
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 4 AS a, 1 b;
+SELECT * FROM mvtest_replace;
+ a | b 
+---+---
+ 4 | 1
+(1 row)
+
+-- Replace table options
+SELECT m.*, c.relname, c.reloptions, s.spcname, a.amname
+  FROM mvtest_replace m
+    CROSS JOIN pg_class c
+    LEFT JOIN pg_tablespace s ON s.oid = c.reltablespace
+    LEFT JOIN pg_am a ON a.oid = c.relam
+  WHERE c.relname = 'mvtest_replace';
+ a | b |    relname     | reloptions | spcname | amname 
+---+---+----------------+------------+---------+--------
+ 4 | 1 | mvtest_replace |            |         | heap
+(1 row)
+
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace
+  USING heap2
+  WITH (fillfactor = 50)
+  TABLESPACE regress_tblspace
+  AS SELECT 5 AS a, 1 AS b;
+SELECT m.*, c.relname, c.reloptions, s.spcname, a.amname
+  FROM mvtest_replace m
+    CROSS JOIN pg_class c
+    LEFT JOIN pg_tablespace s ON s.oid = c.reltablespace
+    LEFT JOIN pg_am a ON a.oid = c.relam
+  WHERE c.relname = 'mvtest_replace';
+ a | b |    relname     |   reloptions    |     spcname      | amname 
+---+---+----------------+-----------------+------------------+--------
+ 5 | 1 | mvtest_replace | {fillfactor=50} | regress_tblspace | heap2
+(1 row)
+
+-- Restore default options
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace
+  AS SELECT 5 AS a, 1 AS b;
+SELECT m.*, c.relname, c.reloptions, s.spcname, a.amname
+  FROM mvtest_replace m
+    CROSS JOIN pg_class c
+    LEFT JOIN pg_tablespace s ON s.oid = c.reltablespace
+    LEFT JOIN pg_am a ON a.oid = c.relam
+  WHERE c.relname = 'mvtest_replace';
+ a | b |    relname     | reloptions | spcname | amname 
+---+---+----------------+------------+---------+--------
+ 5 | 1 | mvtest_replace |            |         | heap
+(1 row)
+
+-- Can replace matview that has a dependent view
+CREATE VIEW mvtest_replace_v AS
+  SELECT * FROM mvtest_replace;
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 6 AS a, 1 AS b;
+SELECT * FROM mvtest_replace, mvtest_replace_v;
+ a | b | a | b 
+---+---+---+---
+ 6 | 1 | 6 | 1
+(1 row)
+
+DROP VIEW mvtest_replace_v;
+-- Index gets rebuilt when replacing with data
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 7 AS a, 1 AS b;
+CREATE UNIQUE INDEX ON mvtest_replace (b);
+SELECT * FROM mvtest_replace;
+ a | b 
+---+---
+ 7 | 1
+(1 row)
+
+SET enable_seqscan = off; -- force index scan
+EXPLAIN (COSTS OFF) SELECT * FROM mvtest_replace WHERE b = 1;
+                       QUERY PLAN                        
+---------------------------------------------------------
+ Index Scan using mvtest_replace_b_idx on mvtest_replace
+   Index Cond: (b = 1)
+(2 rows)
+
+SELECT * FROM mvtest_replace WHERE b = 1;
+ a | b 
+---+---
+ 7 | 1
+(1 row)
+
+RESET enable_seqscan;
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 8 AS a, 1 AS b;
+SET enable_seqscan = off; -- force index scan
+EXPLAIN (COSTS OFF) SELECT * FROM mvtest_replace WHERE b = 1;
+                       QUERY PLAN                        
+---------------------------------------------------------
+ Index Scan using mvtest_replace_b_idx on mvtest_replace
+   Index Cond: (b = 1)
+(2 rows)
+
+SELECT * FROM mvtest_replace WHERE b = 1;
+ a | b 
+---+---
+ 8 | 1
+(1 row)
+
+RESET enable_seqscan;
+-- Cannot change column data type
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 9 AS a, 'x' AS b; -- error
+ERROR:  cannot change data type of materialized view column "b" from integer to text
+SELECT * FROM mvtest_replace;
+ a | b 
+---+---
+ 8 | 1
+(1 row)
+
+-- Cannot rename column
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 10 AS a, 1 AS b2; -- error
+ERROR:  cannot change name of materialized view column "b" to "b2"
+HINT:  Use ALTER MATERIALIZED VIEW ... RENAME COLUMN ... to change name of materialized view column instead.
+SELECT * FROM mvtest_replace;
+ a | b 
+---+---
+ 8 | 1
+(1 row)
+
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 11 AS a, 1 AS b, 'y' COLLATE "C" AS c;
+SELECT * FROM mvtest_replace;
+ a  | b | c 
+----+---+---
+ 11 | 1 | y
+(1 row)
+
+-- Cannot change column collation
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 12 AS a, 1 AS b, 'x' COLLATE "POSIX" AS c; -- error
+ERROR:  cannot change collation of materialized view column "c" from "C" to "POSIX"
+SELECT * FROM mvtest_replace;
+ a  | b | c 
+----+---+---
+ 11 | 1 | y
+(1 row)
+
+-- Cannot drop column
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 13 AS a, 1 AS b; -- error
+ERROR:  cannot drop columns from materialized view
+SELECT * FROM mvtest_replace;
+ a  | b | c 
+----+---+---
+ 11 | 1 | y
+(1 row)
+
+-- Must target a matview
+CREATE VIEW mvtest_not_mv AS
+  SELECT 1 AS a;
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_not_mv AS
+  SELECT 1 AS a; -- error
+ERROR:  "mvtest_not_mv" is not a materialized view
+DROP VIEW mvtest_not_mv;
+-- Cannot use OR REPLACE with IF NOT EXISTS
+CREATE OR REPLACE MATERIALIZED VIEW IF NOT EXISTS mvtest_replace AS
+  SELECT 1 AS a;
+ERROR:  syntax error at or near "NOT"
+LINE 1: CREATE OR REPLACE MATERIALIZED VIEW IF NOT EXISTS mvtest_rep...
+                                               ^
+DROP MATERIALIZED VIEW mvtest_replace;
+-- Clause WITH OLD DATA is not allowed when creating a new matview
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 17 AS a
+  WITH OLD DATA; -- error
+ERROR:  must not specify WITH OLD DATA when creating a new materialized view
+CREATE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 1 AS c1, 2 AS c2, 3 AS c3, 4 AS c4,
+         5 AS c5, 6 AS c6, 7 AS c7, null AS c8;
+-- Add a ninth column (exceeding the old t_bits)
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 1 AS c1, 2 AS c2, 3 AS c3, 4 AS c4,
+         5 AS c5, 6 AS c6, 7 AS c7, null AS c8,
+         null AS c9
+  WITH OLD DATA;
+SELECT c9 FROM mvtest_replace;
+ c9 
+----
+ 
+(1 row)
+
+-- Test constraint violation on WITH OLD DATA
+DROP MATERIALIZED VIEW mvtest_replace;
+CREATE DOMAIN mvtest_dom AS int
+  CONSTRAINT mvtest_dom_nn NOT NULL;
+CREATE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 1::mvtest_dom AS a;
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 2::mvtest_dom AS a, 3::mvtest_dom AS b
+  WITH OLD DATA; -- error: new column "b" cannot be null
+ERROR:  domain mvtest_dom does not allow null values
+SELECT a FROM mvtest_replace;
+ a 
+---
+ 1
+(1 row)
+
diff --git a/src/test/regress/sql/matview.sql b/src/test/regress/sql/matview.sql
index 934426b9ae8..abc354a13f8 100644
--- a/src/test/regress/sql/matview.sql
+++ b/src/test/regress/sql/matview.sql
@@ -318,3 +318,155 @@ EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF)
   CREATE MATERIALIZED VIEW IF NOT EXISTS matview_ine_tab AS
     SELECT 1 / 0 WITH NO DATA; -- ok
 DROP MATERIALIZED VIEW matview_ine_tab;
+
+--
+-- Test CREATE OR REPLACE MATERIALIZED VIEW
+--
+
+-- Matview does not already exist
+DROP MATERIALIZED VIEW IF EXISTS mvtest_replace;
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 1 AS a;
+SELECT * FROM mvtest_replace;
+
+-- Replace query with data
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 2 AS a;
+SELECT * FROM mvtest_replace;
+
+-- Replace query without data
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 3 AS a
+  WITH NO DATA;
+SELECT * FROM mvtest_replace; -- error: not populated
+REFRESH MATERIALIZED VIEW mvtest_replace;
+SELECT * FROM mvtest_replace;
+
+-- Replace query but keep old data
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 5 AS a
+  WITH OLD DATA;
+SELECT * FROM mvtest_replace;
+REFRESH MATERIALIZED VIEW mvtest_replace;
+SELECT * FROM mvtest_replace;
+
+-- Add column
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 4 AS a, 1 b;
+SELECT * FROM mvtest_replace;
+
+-- Replace table options
+SELECT m.*, c.relname, c.reloptions, s.spcname, a.amname
+  FROM mvtest_replace m
+    CROSS JOIN pg_class c
+    LEFT JOIN pg_tablespace s ON s.oid = c.reltablespace
+    LEFT JOIN pg_am a ON a.oid = c.relam
+  WHERE c.relname = 'mvtest_replace';
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace
+  USING heap2
+  WITH (fillfactor = 50)
+  TABLESPACE regress_tblspace
+  AS SELECT 5 AS a, 1 AS b;
+SELECT m.*, c.relname, c.reloptions, s.spcname, a.amname
+  FROM mvtest_replace m
+    CROSS JOIN pg_class c
+    LEFT JOIN pg_tablespace s ON s.oid = c.reltablespace
+    LEFT JOIN pg_am a ON a.oid = c.relam
+  WHERE c.relname = 'mvtest_replace';
+-- Restore default options
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace
+  AS SELECT 5 AS a, 1 AS b;
+SELECT m.*, c.relname, c.reloptions, s.spcname, a.amname
+  FROM mvtest_replace m
+    CROSS JOIN pg_class c
+    LEFT JOIN pg_tablespace s ON s.oid = c.reltablespace
+    LEFT JOIN pg_am a ON a.oid = c.relam
+  WHERE c.relname = 'mvtest_replace';
+
+-- Can replace matview that has a dependent view
+CREATE VIEW mvtest_replace_v AS
+  SELECT * FROM mvtest_replace;
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 6 AS a, 1 AS b;
+SELECT * FROM mvtest_replace, mvtest_replace_v;
+DROP VIEW mvtest_replace_v;
+
+-- Index gets rebuilt when replacing with data
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 7 AS a, 1 AS b;
+CREATE UNIQUE INDEX ON mvtest_replace (b);
+SELECT * FROM mvtest_replace;
+SET enable_seqscan = off; -- force index scan
+EXPLAIN (COSTS OFF) SELECT * FROM mvtest_replace WHERE b = 1;
+SELECT * FROM mvtest_replace WHERE b = 1;
+RESET enable_seqscan;
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 8 AS a, 1 AS b;
+SET enable_seqscan = off; -- force index scan
+EXPLAIN (COSTS OFF) SELECT * FROM mvtest_replace WHERE b = 1;
+SELECT * FROM mvtest_replace WHERE b = 1;
+RESET enable_seqscan;
+
+-- Cannot change column data type
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 9 AS a, 'x' AS b; -- error
+SELECT * FROM mvtest_replace;
+
+-- Cannot rename column
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 10 AS a, 1 AS b2; -- error
+SELECT * FROM mvtest_replace;
+
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 11 AS a, 1 AS b, 'y' COLLATE "C" AS c;
+SELECT * FROM mvtest_replace;
+
+-- Cannot change column collation
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 12 AS a, 1 AS b, 'x' COLLATE "POSIX" AS c; -- error
+SELECT * FROM mvtest_replace;
+
+-- Cannot drop column
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 13 AS a, 1 AS b; -- error
+SELECT * FROM mvtest_replace;
+
+-- Must target a matview
+CREATE VIEW mvtest_not_mv AS
+  SELECT 1 AS a;
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_not_mv AS
+  SELECT 1 AS a; -- error
+DROP VIEW mvtest_not_mv;
+
+-- Cannot use OR REPLACE with IF NOT EXISTS
+CREATE OR REPLACE MATERIALIZED VIEW IF NOT EXISTS mvtest_replace AS
+  SELECT 1 AS a;
+
+DROP MATERIALIZED VIEW mvtest_replace;
+
+-- Clause WITH OLD DATA is not allowed when creating a new matview
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 17 AS a
+  WITH OLD DATA; -- error
+
+CREATE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 1 AS c1, 2 AS c2, 3 AS c3, 4 AS c4,
+         5 AS c5, 6 AS c6, 7 AS c7, null AS c8;
+-- Add a ninth column (exceeding the old t_bits)
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 1 AS c1, 2 AS c2, 3 AS c3, 4 AS c4,
+         5 AS c5, 6 AS c6, 7 AS c7, null AS c8,
+         null AS c9
+  WITH OLD DATA;
+SELECT c9 FROM mvtest_replace;
+
+-- Test constraint violation on WITH OLD DATA
+DROP MATERIALIZED VIEW mvtest_replace;
+CREATE DOMAIN mvtest_dom AS int
+  CONSTRAINT mvtest_dom_nn NOT NULL;
+CREATE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 1::mvtest_dom AS a;
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 2::mvtest_dom AS a, 3::mvtest_dom AS b
+  WITH OLD DATA; -- error: new column "b" cannot be null
+SELECT a FROM mvtest_replace;
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index c5db6ca6705..2223a658899 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -3471,6 +3471,7 @@ WindowStatePerFunc
 WindowStatePerFuncData
 WithCheckOption
 WithClause
+WithDataOption
 WordBoundaryNext
 WordEntry
 WordEntryIN
-- 
2.54.0

Attachments:

  [text/plain] v8-0001-Add-OR-REPLACE-option-to-CREATE-MATERIALIZED-VIEW.patch (38.5K, ../../6d710ac3-3a66-4b31-b35a-0b4d9afdda42@ewie.name/2-v8-0001-Add-OR-REPLACE-option-to-CREATE-MATERIALIZED-VIEW.patch)
  download | inline diff:
From dcf65b62993f5ec831d670761e37004e9eec8e94 Mon Sep 17 00:00:00 2001
From: Erik Wienhold <ewie@ewie.name>
Date: Tue, 21 May 2024 18:35:47 +0200
Subject: [PATCH v8] Add OR REPLACE option to CREATE MATERIALIZED VIEW

Add WITH OLD DATA to keep the current data when replacing a materialized
view which is useful when running REFRESH MATERIALIZED VIEW CONCURRENTLY
afterwards.
---
 .../sgml/ref/create_materialized_view.sgml    |  30 +-
 src/backend/commands/createas.c               | 229 +++++++++++++++-
 src/backend/commands/matview.c                |  80 ++++++
 src/backend/commands/tablecmds.c              |  11 +-
 src/backend/parser/gram.y                     |  25 ++
 src/bin/psql/tab-complete.in.c                |  38 ++-
 src/include/commands/matview.h                |   2 +
 src/include/nodes/parsenodes.h                |   4 +-
 src/include/nodes/primnodes.h                 |  10 +
 src/test/regress/expected/matview.out         | 258 ++++++++++++++++++
 src/test/regress/sql/matview.sql              | 152 +++++++++++
 src/tools/pgindent/typedefs.list              |   1 +
 12 files changed, 813 insertions(+), 27 deletions(-)

diff --git a/doc/src/sgml/ref/create_materialized_view.sgml b/doc/src/sgml/ref/create_materialized_view.sgml
index 62d897931c3..e6c7b7c7d37 100644
--- a/doc/src/sgml/ref/create_materialized_view.sgml
+++ b/doc/src/sgml/ref/create_materialized_view.sgml
@@ -21,13 +21,13 @@ PostgreSQL documentation
 
  <refsynopsisdiv>
 <synopsis>
-CREATE MATERIALIZED VIEW [ IF NOT EXISTS ] <replaceable>table_name</replaceable>
+CREATE [ OR REPLACE ] MATERIALIZED VIEW [ IF NOT EXISTS ] <replaceable>table_name</replaceable>
     [ (<replaceable>column_name</replaceable> [, ...] ) ]
     [ USING <replaceable class="parameter">method</replaceable> ]
     [ WITH ( <replaceable class="parameter">storage_parameter</replaceable> [= <replaceable class="parameter">value</replaceable>] [, ... ] ) ]
     [ TABLESPACE <replaceable class="parameter">tablespace_name</replaceable> ]
     AS <replaceable>query</replaceable>
-    [ WITH [ NO ] DATA ]
+    [ WITH [ NO | OLD ] DATA ]
 </synopsis>
  </refsynopsisdiv>
 
@@ -37,7 +37,8 @@ CREATE MATERIALIZED VIEW [ IF NOT EXISTS ] <replaceable>table_name</replaceable>
   <para>
    <command>CREATE MATERIALIZED VIEW</command> defines a materialized view of
    a query.  The query is executed and used to populate the view at the time
-   the command is issued (unless <command>WITH NO DATA</command> is used) and may be
+   the command is issued (unless <command>WITH NO DATA</command> or
+   <command>WITH OLD DATA</command> is used) and may be
    refreshed later using <command>REFRESH MATERIALIZED VIEW</command>.
   </para>
 
@@ -60,6 +61,17 @@ CREATE MATERIALIZED VIEW [ IF NOT EXISTS ] <replaceable>table_name</replaceable>
   <title>Parameters</title>
 
   <variablelist>
+   <varlistentry>
+    <term><literal>OR REPLACE</literal></term>
+    <listitem>
+     <para>
+      Replaces a materialized view if it already exists.
+      Specifying <literal>OR REPLACE</literal> together with
+      <literal>IF NOT EXISTS</literal> is an error.
+     </para>
+    </listitem>
+   </varlistentry>
+
    <varlistentry>
     <term><literal>IF NOT EXISTS</literal></term>
     <listitem>
@@ -67,7 +79,7 @@ CREATE MATERIALIZED VIEW [ IF NOT EXISTS ] <replaceable>table_name</replaceable>
       Do not throw an error if a materialized view with the same name already
       exists. A notice is issued in this case.  Note that there is no guarantee
       that the existing materialized view is anything like the one that would
-      have been created.
+      have been created, unlike when using <literal>OR REPLACE</literal>.
      </para>
     </listitem>
    </varlistentry>
@@ -151,7 +163,7 @@ CREATE MATERIALIZED VIEW [ IF NOT EXISTS ] <replaceable>table_name</replaceable>
    </varlistentry>
 
    <varlistentry>
-    <term><literal>WITH [ NO ] DATA</literal></term>
+    <term><literal>WITH [ NO | OLD ] DATA</literal></term>
     <listitem>
      <para>
       This clause specifies whether or not the materialized view should be
@@ -159,6 +171,14 @@ CREATE MATERIALIZED VIEW [ IF NOT EXISTS ] <replaceable>table_name</replaceable>
       flagged as unscannable and cannot be queried until <command>REFRESH
       MATERIALIZED VIEW</command> is used.
      </para>
+
+     <para>
+      The form <command>WITH OLD DATA</command> keeps the already stored data
+      when replacing an existing materialized view to keep it populated.  Use
+      this form if you want to use <command>REFRESH MATERIALIZED VIEW CONCURRENTLY</command>
+      as it requires a populated materialized view.  It is an error to use this
+      form when creating a new materialized view.
+     </para>
     </listitem>
    </varlistentry>
 
diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c
index 6dbb831ca89..cbc384dd3f7 100644
--- a/src/backend/commands/createas.c
+++ b/src/backend/commands/createas.c
@@ -34,9 +34,11 @@
 #include "commands/matview.h"
 #include "commands/prepare.h"
 #include "commands/tablecmds.h"
+#include "commands/tablespace.h"
 #include "commands/view.h"
 #include "executor/execdesc.h"
 #include "executor/executor.h"
+#include "miscadmin.h"
 #include "nodes/makefuncs.h"
 #include "nodes/nodeFuncs.h"
 #include "nodes/queryjumble.h"
@@ -63,6 +65,7 @@ typedef struct
 /* utility functions for CTAS definition creation */
 static ObjectAddress create_ctas_internal(List *attrList, IntoClause *into);
 static ObjectAddress create_ctas_nodata(List *tlist, IntoClause *into);
+static ObjectAddress create_ctas_replace(List *tlist, IntoClause *into, Oid matviewOid);
 
 /* DestReceiver routines for collecting data */
 static void intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo);
@@ -70,6 +73,7 @@ static bool intorel_receive(TupleTableSlot *slot, DestReceiver *self);
 static void intorel_shutdown(DestReceiver *self);
 static void intorel_destroy(DestReceiver *self);
 
+static bool CreateTableAsRelReplaceable(CreateTableAsStmt *ctas);
 
 /*
  * create_ctas_internal
@@ -157,6 +161,8 @@ create_ctas_nodata(List *tlist, IntoClause *into)
 	List	   *attrList;
 	ListCell   *t,
 			   *lc;
+	bool		is_matview = (into->viewQuery != NULL);
+	Oid			matviewOid = InvalidOid;
 
 	/*
 	 * Build list of ColumnDefs from non-junk elements of the tlist.  If a
@@ -211,8 +217,146 @@ create_ctas_nodata(List *tlist, IntoClause *into)
 				(errcode(ERRCODE_SYNTAX_ERROR),
 				 errmsg("too many column names were specified")));
 
-	/* Create the relation definition using the ColumnDef list */
-	return create_ctas_internal(attrList, into);
+	/* Get the existing matview to be replaced */
+	if (is_matview && into->replace)
+		(void) RangeVarGetAndCheckCreationNamespace(into->rel,
+													AccessExclusiveLock,
+													&matviewOid);
+
+	if (OidIsValid(matviewOid))
+		/* Replace the existing matview */
+		return create_ctas_replace(attrList, into, matviewOid);
+	else
+		/* Create the relation definition using the ColumnDef list */
+		return create_ctas_internal(attrList, into);
+}
+
+
+/*
+ * create_ctas_replace
+ *
+ * Internal utility used for replacing the definition of a materialized view.
+ * Caller needs to provide a list of attributes (ColumnDef nodes) and the
+ * materialized view OID.
+ */
+static ObjectAddress
+create_ctas_replace(List *attrList, IntoClause *into, Oid matviewOid)
+{
+	ObjectAddress intoRelationAddr;
+	Relation	rel;
+	List	   *atcmds = NIL;
+	AlterTableCmd *atcmd;
+	TupleDesc	descriptor;
+	Query	   *query;
+
+	/* Relation is already locked, but we must build a relcache entry. */
+	rel = relation_open(matviewOid, NoLock);
+
+	/* Make sure it *is* a matview. */
+	if (rel->rd_rel->relkind != RELKIND_MATVIEW)
+		ereport(ERROR,
+				errcode(ERRCODE_WRONG_OBJECT_TYPE),
+				errmsg("\"%s\" is not a materialized view",
+					   RelationGetRelationName(rel)));
+
+	/* Also check it's not in use already */
+	CheckTableNotInUse(rel, "CREATE OR REPLACE MATERIALIZED VIEW");
+
+	descriptor = BuildDescForRelation(attrList);
+	checkMatviewColumns(descriptor, rel->rd_att);
+
+	/*
+	 * If new attributes have been added, we must add pg_attribute entries for
+	 * them.  It is convenient (although overkill) to use the ALTER TABLE ADD
+	 * COLUMN infrastructure for this.
+	 *
+	 * Note that we must do this before updating the query for the matview,
+	 * since the rules system requires that the correct matview columns be in
+	 * place when defining the new rules.
+	 *
+	 * Also note that ALTER TABLE doesn't run parse transformation on
+	 * AT_AddColumnToView commands.  The ColumnDef we supply must be ready to
+	 * execute as-is.
+	 */
+	if (list_length(attrList) > rel->rd_att->natts)
+	{
+		ListCell   *c;
+
+		for_each_from(c, attrList, rel->rd_att->natts)
+		{
+			atcmd = makeNode(AlterTableCmd);
+			atcmd->subtype = AT_AddColumnToView;
+			atcmd->def = (Node *) lfirst(c);
+			atcmds = lappend(atcmds, atcmd);
+		}
+	}
+
+	/*
+	 * Use ALTER TABLE to set access method, tablespace, and storage options.
+	 * When replacing an existing matview we need to alter the relation such
+	 * that the defaults apply as if they have not been specified at all by
+	 * the CREATE OR REPLACE MATERIALIZED VIEW statement.
+	 */
+
+	/* access method */
+	atcmd = makeNode(AlterTableCmd);
+	atcmd->subtype = AT_SetAccessMethod;
+	atcmd->name = into->accessMethod
+		? into->accessMethod : default_table_access_method;
+	atcmds = lappend(atcmds, atcmd);
+
+	/* tablespace */
+	atcmd = makeNode(AlterTableCmd);
+	atcmd->subtype = AT_SetTableSpace;
+	if (into->tableSpaceName)
+		atcmd->name = into->tableSpaceName;
+	else
+	{
+		Oid			spcOid;
+		char	   *spcName;
+
+		/*
+		 * Must use the default tablespace if no explicit tablespace is
+		 * specified.
+		 */
+		spcOid = GetDefaultTablespace(RELPERSISTENCE_PERMANENT, false);
+		if (!OidIsValid(spcOid))
+			spcOid = MyDatabaseTableSpace;
+
+		spcName = get_tablespace_name(spcOid);
+		if (!spcName)			/* should not happen */
+			elog(ERROR, "could not find tuple for tablespace %u", spcOid);
+
+		atcmd->name = spcName;
+	}
+	atcmds = lappend(atcmds, atcmd);
+
+	/* storage options */
+	atcmd = makeNode(AlterTableCmd);
+	atcmd->subtype = AT_ReplaceRelOptions;
+	atcmd->def = (Node *) into->options;
+	atcmds = lappend(atcmds, atcmd);
+
+	/* EventTriggerAlterTableStart called by ProcessUtilitySlow */
+	AlterTableInternal(matviewOid, atcmds, true);
+
+	/* Make the new matview columns visible */
+	CommandCounterIncrement();
+
+	relation_close(rel, NoLock);
+	ObjectAddressSet(intoRelationAddr, RelationRelationId, matviewOid);
+
+	/*
+	 * Replace the "view" part of the matview.  StoreViewQuery scribbles on
+	 * tree, so make a copy.
+	 */
+	query = copyObject(into->viewQuery);
+	StoreViewQuery(intoRelationAddr.objectId, query, true);
+
+	/* Make the new matview query visible */
+	CommandCounterIncrement();
+
+	return intoRelationAddr;
 }
 
 
@@ -232,8 +376,37 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt,
 	DestReceiver *dest;
 	ObjectAddress address;
 
-	/* Check if the relation exists or not */
-	if (CreateTableAsRelExists(stmt))
+	/*
+	 * Check if the relation exists or not.  An existing materialized view can
+	 * be replaced.
+	 */
+	if (is_matview && into->replace)
+	{
+		if (CreateTableAsRelReplaceable(stmt))
+		{
+			/* Change the relation to match the new query and other options. */
+			address = create_ctas_nodata(query->targetList, into);
+
+			/*
+			 * Refresh the materialized view with a fake statement unless we
+			 * must keep the old data.
+			 */
+			if (into->data != WITHDATA_OLD)
+			{
+				RefreshMatViewStmt *refresh;
+
+				refresh = makeNode(RefreshMatViewStmt);
+				refresh->relation = into->rel;
+				refresh->skipData = into->skipData;
+				refresh->concurrent = false;
+
+				address = ExecRefreshMatView(refresh, pstate->p_sourcetext, qc);
+			}
+
+			return address;
+		}
+	}
+	else if (CreateTableAsRelExists(stmt))
 		return InvalidObjectAddress;
 
 	/*
@@ -273,6 +446,11 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt,
 	 */
 	if (is_matview)
 	{
+		if (into->data == WITHDATA_OLD)
+			ereport(ERROR,
+					(errcode(ERRCODE_SYNTAX_ERROR),
+					 errmsg("must not specify WITH OLD DATA when creating a new materialized view")));
+
 		do_refresh = !into->skipData;
 		into->skipData = true;
 	}
@@ -429,6 +607,49 @@ CreateTableAsRelExists(CreateTableAsStmt *ctas)
 	return false;
 }
 
+/*
+ * CreateTableAsRelReplaceable --- check existence of replaceable relation for
+ * CreateTableAsStmt
+ *
+ * Utility wrapper checking if the relation pending for creation in this
+ * CreateTableAsStmt query already exists or not.  Returns true if the relation
+ * exists and should be replaced, otherwise false.
+ */
+static bool
+CreateTableAsRelReplaceable(CreateTableAsStmt *ctas)
+{
+	Oid			nspid;
+	Oid			oldrelid;
+	ObjectAddress address;
+	IntoClause *into = ctas->into;
+
+	nspid = RangeVarGetCreationNamespace(into->rel);
+
+	oldrelid = get_relname_relid(into->rel->relname, nspid);
+	if (OidIsValid(oldrelid))
+	{
+		if (!into->replace)
+			ereport(ERROR,
+					(errcode(ERRCODE_DUPLICATE_TABLE),
+					 errmsg("relation \"%s\" already exists",
+							into->rel->relname)));
+
+		/*
+		 * The relation exists and OR REPLACE has been specified.
+		 *
+		 * If we are in an extension script, insist that the pre-existing
+		 * object be a member of the extension, to avoid security risks.
+		 */
+		ObjectAddressSet(address, RelationRelationId, oldrelid);
+		checkMembershipInCurrentExtension(&address);
+
+		return true;
+	}
+
+	/* Relation does not exist, it can be created */
+	return false;
+}
+
 /*
  * CreateIntoRelDestReceiver -- create a suitable DestReceiver object
  *
diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c
index f7d8007f796..853eae37057 100644
--- a/src/backend/commands/matview.c
+++ b/src/backend/commands/matview.c
@@ -967,3 +967,83 @@ CloseMatViewIncrementalMaintenance(void)
 	matview_maintenance_depth--;
 	Assert(matview_maintenance_depth >= 0);
 }
+
+/*
+ * Verify that the columns associated with proposed new matview definition
+ * match the columns of the old matview.  This is similar to equalRowTypes(),
+ * with code added to generate specific complaints.  Also, we allow the new
+ * matview to have more columns than the old.
+ */
+void
+checkMatviewColumns(TupleDesc newdesc, TupleDesc olddesc)
+{
+	int			i;
+
+	if (newdesc->natts < olddesc->natts)
+	{
+		ereport(ERROR,
+				errcode(ERRCODE_INVALID_TABLE_DEFINITION),
+				errmsg("cannot drop columns from materialized view"));
+	}
+
+	for (i = 0; i < olddesc->natts; i++)
+	{
+		Form_pg_attribute newattr = TupleDescAttr(newdesc, i);
+		Form_pg_attribute oldattr = TupleDescAttr(olddesc, i);
+
+		/* XXX msg not right, but we don't support DROP COL on matview anyway */
+		if (newattr->attisdropped != oldattr->attisdropped)
+		{
+			ereport(ERROR,
+					errcode(ERRCODE_INVALID_TABLE_DEFINITION),
+					errmsg("cannot drop columns from materialized view"));
+		}
+
+		if (strcmp(NameStr(newattr->attname), NameStr(oldattr->attname)) != 0)
+		{
+			ereport(ERROR,
+					errcode(ERRCODE_INVALID_TABLE_DEFINITION),
+					errmsg("cannot change name of materialized view column \"%s\" to \"%s\"",
+						   NameStr(oldattr->attname),
+						   NameStr(newattr->attname)),
+					errhint("Use ALTER MATERIALIZED VIEW ... RENAME COLUMN ... to change name of materialized view column instead."));
+		}
+
+		/*
+		 * We cannot allow type, typmod, or collation to change, since these
+		 * properties may be embedded in Vars of other views/rules referencing
+		 * this one.  Other column attributes can be ignored.
+		 */
+		if (newattr->atttypid != oldattr->atttypid ||
+			newattr->atttypmod != oldattr->atttypmod)
+		{
+			ereport(ERROR,
+					errcode(ERRCODE_INVALID_TABLE_DEFINITION),
+					errmsg("cannot change data type of materialized view column \"%s\" from %s to %s",
+						   NameStr(oldattr->attname),
+						   format_type_with_typemod(oldattr->atttypid,
+													oldattr->atttypmod),
+						   format_type_with_typemod(newattr->atttypid,
+													newattr->atttypmod)));
+		}
+
+		/*
+		 * At this point, attcollations should be both valid or both invalid,
+		 * so applying get_collation_name unconditionally should be fine.
+		 */
+		if (newattr->attcollation != oldattr->attcollation)
+		{
+			ereport(ERROR,
+					errcode(ERRCODE_INVALID_TABLE_DEFINITION),
+					errmsg("cannot change collation of materialized view column \"%s\" from \"%s\" to \"%s\"",
+						   NameStr(oldattr->attname),
+						   get_collation_name(oldattr->attcollation),
+						   get_collation_name(newattr->attcollation)));
+		}
+	}
+
+	/*
+	 * We ignore the constraint fields since the new matview desc can't have
+	 * any constraints.
+	 */
+}
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 472db112fa7..5bf322257c5 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -4720,7 +4720,8 @@ AlterTableGetLockLevel(List *cmds)
 				 * Subcommands that may be visible to concurrent SELECTs
 				 */
 			case AT_DropColumn: /* change visible to SELECT */
-			case AT_AddColumnToView:	/* CREATE VIEW */
+			case AT_AddColumnToView:	/* via CREATE OR REPLACE
+										 * [MATERIALIZED] VIEW */
 			case AT_DropOids:	/* used to equiv to DropColumn */
 			case AT_EnableAlwaysRule:	/* may change SELECT rules */
 			case AT_EnableReplicaRule:	/* may change SELECT rules */
@@ -5020,8 +5021,9 @@ ATPrepCmd(List **wqueue, Relation rel, AlterTableCmd *cmd,
 			/* Recursion occurs during execution phase */
 			pass = AT_PASS_ADD_COL;
 			break;
-		case AT_AddColumnToView:	/* add column via CREATE OR REPLACE VIEW */
-			ATSimplePermissions(cmd->subtype, rel, ATT_VIEW);
+		case AT_AddColumnToView:	/* via CREATE OR REPLACE [MATERIALIZED]
+									 * VIEW */
+			ATSimplePermissions(cmd->subtype, rel, ATT_VIEW | ATT_MATVIEW);
 			ATPrepAddColumn(wqueue, rel, recurse, recursing, true, cmd,
 							lockmode, context);
 			/* Recursion occurs during execution phase */
@@ -5458,7 +5460,8 @@ ATExecCmd(List **wqueue, AlteredTableInfo *tab,
 	switch (cmd->subtype)
 	{
 		case AT_AddColumn:		/* ADD COLUMN */
-		case AT_AddColumnToView:	/* add column via CREATE OR REPLACE VIEW */
+		case AT_AddColumnToView:	/* via CREATE OR REPLACE [MATERIALIZED]
+									 * VIEW */
 			address = ATExecAddColumn(wqueue, tab, rel, &cmd,
 									  cmd->recurse, false,
 									  lockmode, cur_pass, context);
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index ff4e1388c55..91e52689d7a 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -250,6 +250,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 	Alias	   *alias;
 	RangeVar   *range;
 	IntoClause *into;
+	WithDataOption withdata;
 	WithClause *with;
 	InferClause	*infer;
 	OnConflictClause *onconflict;
@@ -358,6 +359,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 				opt_grant_grant_option
 				opt_nowait opt_if_exists opt_with_data
 				opt_transaction_chain
+%type <withdata> opt_with_no_or_old_data
 %type <list>	grant_role_opt_list
 %type <defelt>	grant_role_opt
 %type <node>	grant_role_opt_value
@@ -5030,6 +5032,13 @@ opt_with_data:
 			| /*EMPTY*/								{ $$ = true; }
 		;
 
+opt_with_no_or_old_data:
+			WITH DATA_P								{ $$ = WITHDATA_DEFAULT; }
+			| WITH NO DATA_P						{ $$ = WITHDATA_NONE; }
+			| WITH OLD DATA_P						{ $$ = WITHDATA_OLD; }
+			| /*EMPTY*/								{ $$ = WITHDATA_DEFAULT; }
+		;
+
 
 /*****************************************************************************
  *
@@ -5067,6 +5076,22 @@ CreateMatViewStmt:
 					$8->skipData = !($11);
 					$$ = (Node *) ctas;
 				}
+		| CREATE OR REPLACE OptNoLog MATERIALIZED VIEW create_mv_target AS SelectStmt opt_with_no_or_old_data
+				{
+					CreateTableAsStmt *ctas = makeNode(CreateTableAsStmt);
+
+					ctas->query = $9;
+					ctas->into = $7;
+					ctas->objtype = OBJECT_MATVIEW;
+					ctas->is_select_into = false;
+					ctas->if_not_exists = false;
+					/* cram additional flags into the IntoClause */
+					$7->rel->relpersistence = $4;
+					$7->skipData = $10 == WITHDATA_NONE;
+					$7->data = $10;
+					$7->replace = true;
+					$$ = (Node *) ctas;
+				}
 		;
 
 create_mv_target:
diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c
index 46b9add0604..e683e4b1b33 100644
--- a/src/bin/psql/tab-complete.in.c
+++ b/src/bin/psql/tab-complete.in.c
@@ -2225,7 +2225,7 @@ match_previous_words(int pattern_id,
 	/* complete with something you can create or replace */
 	else if (TailMatches("CREATE", "OR", "REPLACE"))
 		COMPLETE_WITH("FUNCTION", "PROCEDURE", "LANGUAGE", "RULE", "VIEW",
-					  "AGGREGATE", "TRANSFORM", "TRIGGER");
+					  "AGGREGATE", "TRANSFORM", "TRIGGER", "MATERIALIZED VIEW");
 
 /* DROP, but not DROP embedded in other commands */
 	/* complete with something you can drop */
@@ -4256,28 +4256,42 @@ match_previous_words(int pattern_id,
 		COMPLETE_WITH("SELECT");
 
 /* CREATE MATERIALIZED VIEW */
-	else if (Matches("CREATE", "MATERIALIZED"))
+	else if (Matches("CREATE", "MATERIALIZED") ||
+			 Matches("CREATE", "OR", "REPLACE", "MATERIALIZED"))
 		COMPLETE_WITH("VIEW");
-	/* Complete CREATE MATERIALIZED VIEW <name> with AS or USING */
-	else if (Matches("CREATE", "MATERIALIZED", "VIEW", MatchAny))
+
+	/*
+	 * Complete CREATE [ OR REPLACE ] MATERIALIZED VIEW <name> with AS or
+	 * USING
+	 */
+	else if (Matches("CREATE", "MATERIALIZED", "VIEW", MatchAny) ||
+			 Matches("CREATE", "OR", "REPLACE", "MATERIALIZED", "VIEW", MatchAny))
 		COMPLETE_WITH("AS", "USING");
 
 	/*
-	 * Complete CREATE MATERIALIZED VIEW <name> USING with list of access
-	 * methods
+	 * Complete CREATE [ OR REPLACE ] MATERIALIZED VIEW <name> USING with list
+	 * of access methods
 	 */
-	else if (Matches("CREATE", "MATERIALIZED", "VIEW", MatchAny, "USING"))
+	else if (Matches("CREATE", "MATERIALIZED", "VIEW", MatchAny, "USING") ||
+			 Matches("CREATE", "OR", "REPLACE", "MATERIALIZED", "VIEW", MatchAny, "USING"))
 		COMPLETE_WITH_QUERY(Query_for_list_of_table_access_methods);
-	/* Complete CREATE MATERIALIZED VIEW <name> USING <access method> with AS */
-	else if (Matches("CREATE", "MATERIALIZED", "VIEW", MatchAny, "USING", MatchAny))
+
+	/*
+	 * Complete CREATE [ OR REPLACE ] MATERIALIZED VIEW <name> USING <access
+	 * method> with AS
+	 */
+	else if (Matches("CREATE", "MATERIALIZED", "VIEW", MatchAny, "USING", MatchAny) ||
+			 Matches("CREATE", "OR", "REPLACE", "MATERIALIZED", "VIEW", MatchAny, "USING", MatchAny))
 		COMPLETE_WITH("AS");
 
 	/*
-	 * Complete CREATE MATERIALIZED VIEW <name> [USING <access method> ] AS
-	 * with "SELECT"
+	 * Complete CREATE [ OR REPLACE ] MATERIALIZED VIEW <name> [USING <access
+	 * method> ] AS with "SELECT"
 	 */
 	else if (Matches("CREATE", "MATERIALIZED", "VIEW", MatchAny, "AS") ||
-			 Matches("CREATE", "MATERIALIZED", "VIEW", MatchAny, "USING", MatchAny, "AS"))
+			 Matches("CREATE", "OR", "REPLACE", "MATERIALIZED", "VIEW", MatchAny, "AS") ||
+			 Matches("CREATE", "MATERIALIZED", "VIEW", MatchAny, "USING", MatchAny, "AS") ||
+			 Matches("CREATE", "OR", "REPLACE", "MATERIALIZED", "VIEW", MatchAny, "USING", MatchAny, "AS"))
 		COMPLETE_WITH("SELECT");
 
 /* CREATE EVENT TRIGGER */
diff --git a/src/include/commands/matview.h b/src/include/commands/matview.h
index 738c731c1a9..140f7e96696 100644
--- a/src/include/commands/matview.h
+++ b/src/include/commands/matview.h
@@ -33,4 +33,6 @@ extern DestReceiver *CreateTransientRelDestReceiver(Oid transientoid);
 
 extern bool MatViewIncrementalMaintenanceIsEnabled(void);
 
+extern void checkMatviewColumns(TupleDesc newdesc, TupleDesc olddesc);
+
 #endif							/* MATVIEW_H */
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 4133c404a6b..7c629d09fb1 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -2519,7 +2519,8 @@ typedef struct AlterTableStmt
 typedef enum AlterTableType
 {
 	AT_AddColumn,				/* add column */
-	AT_AddColumnToView,			/* implicitly via CREATE OR REPLACE VIEW */
+	AT_AddColumnToView,			/* implicitly via CREATE OR REPLACE
+								 * [MATERIALIZED] VIEW */
 	AT_ColumnDefault,			/* alter column default */
 	AT_CookedColumnDefault,		/* add a pre-cooked column default */
 	AT_DropNotNull,				/* alter column drop not null */
@@ -4595,5 +4596,4 @@ typedef struct WaitStmt
 	List	   *options;		/* List of DefElem nodes */
 } WaitStmt;
 
-
 #endif							/* PARSENODES_H */
diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h
index bb05aeebee4..3b916993dbe 100644
--- a/src/include/nodes/primnodes.h
+++ b/src/include/nodes/primnodes.h
@@ -147,6 +147,14 @@ typedef struct TableFunc
 	ParseLoc	location;
 } TableFunc;
 
+/* WITH DATA option of CREATE MATERIALIZED VIEW */
+typedef enum WithDataOption
+{
+	WITHDATA_DEFAULT,			/* WITH DATA */
+	WITHDATA_NONE,				/* WITH NO DATA */
+	WITHDATA_OLD,				/* WITH OLD DATA */
+} WithDataOption;
+
 /*
  * IntoClause - target information for SELECT INTO, CREATE TABLE AS, and
  * CREATE MATERIALIZED VIEW
@@ -170,6 +178,8 @@ typedef struct IntoClause
 	/* materialized view's SELECT query */
 	struct Query *viewQuery pg_node_attr(query_jumble_ignore);
 	bool		skipData;		/* true for WITH NO DATA */
+	WithDataOption data;		/* WITH [ NO | OLD ] DATA */
+	bool		replace;		/* replace existing matview? */
 } IntoClause;
 
 
diff --git a/src/test/regress/expected/matview.out b/src/test/regress/expected/matview.out
index 0355720dfc6..e923aa4f0a7 100644
--- a/src/test/regress/expected/matview.out
+++ b/src/test/regress/expected/matview.out
@@ -699,3 +699,261 @@ NOTICE:  relation "matview_ine_tab" already exists, skipping
 (0 rows)
 
 DROP MATERIALIZED VIEW matview_ine_tab;
+--
+-- Test CREATE OR REPLACE MATERIALIZED VIEW
+--
+-- Matview does not already exist
+DROP MATERIALIZED VIEW IF EXISTS mvtest_replace;
+NOTICE:  materialized view "mvtest_replace" does not exist, skipping
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 1 AS a;
+SELECT * FROM mvtest_replace;
+ a 
+---
+ 1
+(1 row)
+
+-- Replace query with data
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 2 AS a;
+SELECT * FROM mvtest_replace;
+ a 
+---
+ 2
+(1 row)
+
+-- Replace query without data
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 3 AS a
+  WITH NO DATA;
+SELECT * FROM mvtest_replace; -- error: not populated
+ERROR:  materialized view "mvtest_replace" has not been populated
+HINT:  Use the REFRESH MATERIALIZED VIEW command.
+REFRESH MATERIALIZED VIEW mvtest_replace;
+SELECT * FROM mvtest_replace;
+ a 
+---
+ 3
+(1 row)
+
+-- Replace query but keep old data
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 5 AS a
+  WITH OLD DATA;
+SELECT * FROM mvtest_replace;
+ a 
+---
+ 3
+(1 row)
+
+REFRESH MATERIALIZED VIEW mvtest_replace;
+SELECT * FROM mvtest_replace;
+ a 
+---
+ 5
+(1 row)
+
+-- Add column
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 4 AS a, 1 b;
+SELECT * FROM mvtest_replace;
+ a | b 
+---+---
+ 4 | 1
+(1 row)
+
+-- Replace table options
+SELECT m.*, c.relname, c.reloptions, s.spcname, a.amname
+  FROM mvtest_replace m
+    CROSS JOIN pg_class c
+    LEFT JOIN pg_tablespace s ON s.oid = c.reltablespace
+    LEFT JOIN pg_am a ON a.oid = c.relam
+  WHERE c.relname = 'mvtest_replace';
+ a | b |    relname     | reloptions | spcname | amname 
+---+---+----------------+------------+---------+--------
+ 4 | 1 | mvtest_replace |            |         | heap
+(1 row)
+
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace
+  USING heap2
+  WITH (fillfactor = 50)
+  TABLESPACE regress_tblspace
+  AS SELECT 5 AS a, 1 AS b;
+SELECT m.*, c.relname, c.reloptions, s.spcname, a.amname
+  FROM mvtest_replace m
+    CROSS JOIN pg_class c
+    LEFT JOIN pg_tablespace s ON s.oid = c.reltablespace
+    LEFT JOIN pg_am a ON a.oid = c.relam
+  WHERE c.relname = 'mvtest_replace';
+ a | b |    relname     |   reloptions    |     spcname      | amname 
+---+---+----------------+-----------------+------------------+--------
+ 5 | 1 | mvtest_replace | {fillfactor=50} | regress_tblspace | heap2
+(1 row)
+
+-- Restore default options
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace
+  AS SELECT 5 AS a, 1 AS b;
+SELECT m.*, c.relname, c.reloptions, s.spcname, a.amname
+  FROM mvtest_replace m
+    CROSS JOIN pg_class c
+    LEFT JOIN pg_tablespace s ON s.oid = c.reltablespace
+    LEFT JOIN pg_am a ON a.oid = c.relam
+  WHERE c.relname = 'mvtest_replace';
+ a | b |    relname     | reloptions | spcname | amname 
+---+---+----------------+------------+---------+--------
+ 5 | 1 | mvtest_replace |            |         | heap
+(1 row)
+
+-- Can replace matview that has a dependent view
+CREATE VIEW mvtest_replace_v AS
+  SELECT * FROM mvtest_replace;
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 6 AS a, 1 AS b;
+SELECT * FROM mvtest_replace, mvtest_replace_v;
+ a | b | a | b 
+---+---+---+---
+ 6 | 1 | 6 | 1
+(1 row)
+
+DROP VIEW mvtest_replace_v;
+-- Index gets rebuilt when replacing with data
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 7 AS a, 1 AS b;
+CREATE UNIQUE INDEX ON mvtest_replace (b);
+SELECT * FROM mvtest_replace;
+ a | b 
+---+---
+ 7 | 1
+(1 row)
+
+SET enable_seqscan = off; -- force index scan
+EXPLAIN (COSTS OFF) SELECT * FROM mvtest_replace WHERE b = 1;
+                       QUERY PLAN                        
+---------------------------------------------------------
+ Index Scan using mvtest_replace_b_idx on mvtest_replace
+   Index Cond: (b = 1)
+(2 rows)
+
+SELECT * FROM mvtest_replace WHERE b = 1;
+ a | b 
+---+---
+ 7 | 1
+(1 row)
+
+RESET enable_seqscan;
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 8 AS a, 1 AS b;
+SET enable_seqscan = off; -- force index scan
+EXPLAIN (COSTS OFF) SELECT * FROM mvtest_replace WHERE b = 1;
+                       QUERY PLAN                        
+---------------------------------------------------------
+ Index Scan using mvtest_replace_b_idx on mvtest_replace
+   Index Cond: (b = 1)
+(2 rows)
+
+SELECT * FROM mvtest_replace WHERE b = 1;
+ a | b 
+---+---
+ 8 | 1
+(1 row)
+
+RESET enable_seqscan;
+-- Cannot change column data type
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 9 AS a, 'x' AS b; -- error
+ERROR:  cannot change data type of materialized view column "b" from integer to text
+SELECT * FROM mvtest_replace;
+ a | b 
+---+---
+ 8 | 1
+(1 row)
+
+-- Cannot rename column
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 10 AS a, 1 AS b2; -- error
+ERROR:  cannot change name of materialized view column "b" to "b2"
+HINT:  Use ALTER MATERIALIZED VIEW ... RENAME COLUMN ... to change name of materialized view column instead.
+SELECT * FROM mvtest_replace;
+ a | b 
+---+---
+ 8 | 1
+(1 row)
+
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 11 AS a, 1 AS b, 'y' COLLATE "C" AS c;
+SELECT * FROM mvtest_replace;
+ a  | b | c 
+----+---+---
+ 11 | 1 | y
+(1 row)
+
+-- Cannot change column collation
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 12 AS a, 1 AS b, 'x' COLLATE "POSIX" AS c; -- error
+ERROR:  cannot change collation of materialized view column "c" from "C" to "POSIX"
+SELECT * FROM mvtest_replace;
+ a  | b | c 
+----+---+---
+ 11 | 1 | y
+(1 row)
+
+-- Cannot drop column
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 13 AS a, 1 AS b; -- error
+ERROR:  cannot drop columns from materialized view
+SELECT * FROM mvtest_replace;
+ a  | b | c 
+----+---+---
+ 11 | 1 | y
+(1 row)
+
+-- Must target a matview
+CREATE VIEW mvtest_not_mv AS
+  SELECT 1 AS a;
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_not_mv AS
+  SELECT 1 AS a; -- error
+ERROR:  "mvtest_not_mv" is not a materialized view
+DROP VIEW mvtest_not_mv;
+-- Cannot use OR REPLACE with IF NOT EXISTS
+CREATE OR REPLACE MATERIALIZED VIEW IF NOT EXISTS mvtest_replace AS
+  SELECT 1 AS a;
+ERROR:  syntax error at or near "NOT"
+LINE 1: CREATE OR REPLACE MATERIALIZED VIEW IF NOT EXISTS mvtest_rep...
+                                               ^
+DROP MATERIALIZED VIEW mvtest_replace;
+-- Clause WITH OLD DATA is not allowed when creating a new matview
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 17 AS a
+  WITH OLD DATA; -- error
+ERROR:  must not specify WITH OLD DATA when creating a new materialized view
+CREATE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 1 AS c1, 2 AS c2, 3 AS c3, 4 AS c4,
+         5 AS c5, 6 AS c6, 7 AS c7, null AS c8;
+-- Add a ninth column (exceeding the old t_bits)
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 1 AS c1, 2 AS c2, 3 AS c3, 4 AS c4,
+         5 AS c5, 6 AS c6, 7 AS c7, null AS c8,
+         null AS c9
+  WITH OLD DATA;
+SELECT c9 FROM mvtest_replace;
+ c9 
+----
+ 
+(1 row)
+
+-- Test constraint violation on WITH OLD DATA
+DROP MATERIALIZED VIEW mvtest_replace;
+CREATE DOMAIN mvtest_dom AS int
+  CONSTRAINT mvtest_dom_nn NOT NULL;
+CREATE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 1::mvtest_dom AS a;
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 2::mvtest_dom AS a, 3::mvtest_dom AS b
+  WITH OLD DATA; -- error: new column "b" cannot be null
+ERROR:  domain mvtest_dom does not allow null values
+SELECT a FROM mvtest_replace;
+ a 
+---
+ 1
+(1 row)
+
diff --git a/src/test/regress/sql/matview.sql b/src/test/regress/sql/matview.sql
index 934426b9ae8..abc354a13f8 100644
--- a/src/test/regress/sql/matview.sql
+++ b/src/test/regress/sql/matview.sql
@@ -318,3 +318,155 @@ EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF)
   CREATE MATERIALIZED VIEW IF NOT EXISTS matview_ine_tab AS
     SELECT 1 / 0 WITH NO DATA; -- ok
 DROP MATERIALIZED VIEW matview_ine_tab;
+
+--
+-- Test CREATE OR REPLACE MATERIALIZED VIEW
+--
+
+-- Matview does not already exist
+DROP MATERIALIZED VIEW IF EXISTS mvtest_replace;
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 1 AS a;
+SELECT * FROM mvtest_replace;
+
+-- Replace query with data
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 2 AS a;
+SELECT * FROM mvtest_replace;
+
+-- Replace query without data
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 3 AS a
+  WITH NO DATA;
+SELECT * FROM mvtest_replace; -- error: not populated
+REFRESH MATERIALIZED VIEW mvtest_replace;
+SELECT * FROM mvtest_replace;
+
+-- Replace query but keep old data
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 5 AS a
+  WITH OLD DATA;
+SELECT * FROM mvtest_replace;
+REFRESH MATERIALIZED VIEW mvtest_replace;
+SELECT * FROM mvtest_replace;
+
+-- Add column
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 4 AS a, 1 b;
+SELECT * FROM mvtest_replace;
+
+-- Replace table options
+SELECT m.*, c.relname, c.reloptions, s.spcname, a.amname
+  FROM mvtest_replace m
+    CROSS JOIN pg_class c
+    LEFT JOIN pg_tablespace s ON s.oid = c.reltablespace
+    LEFT JOIN pg_am a ON a.oid = c.relam
+  WHERE c.relname = 'mvtest_replace';
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace
+  USING heap2
+  WITH (fillfactor = 50)
+  TABLESPACE regress_tblspace
+  AS SELECT 5 AS a, 1 AS b;
+SELECT m.*, c.relname, c.reloptions, s.spcname, a.amname
+  FROM mvtest_replace m
+    CROSS JOIN pg_class c
+    LEFT JOIN pg_tablespace s ON s.oid = c.reltablespace
+    LEFT JOIN pg_am a ON a.oid = c.relam
+  WHERE c.relname = 'mvtest_replace';
+-- Restore default options
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace
+  AS SELECT 5 AS a, 1 AS b;
+SELECT m.*, c.relname, c.reloptions, s.spcname, a.amname
+  FROM mvtest_replace m
+    CROSS JOIN pg_class c
+    LEFT JOIN pg_tablespace s ON s.oid = c.reltablespace
+    LEFT JOIN pg_am a ON a.oid = c.relam
+  WHERE c.relname = 'mvtest_replace';
+
+-- Can replace matview that has a dependent view
+CREATE VIEW mvtest_replace_v AS
+  SELECT * FROM mvtest_replace;
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 6 AS a, 1 AS b;
+SELECT * FROM mvtest_replace, mvtest_replace_v;
+DROP VIEW mvtest_replace_v;
+
+-- Index gets rebuilt when replacing with data
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 7 AS a, 1 AS b;
+CREATE UNIQUE INDEX ON mvtest_replace (b);
+SELECT * FROM mvtest_replace;
+SET enable_seqscan = off; -- force index scan
+EXPLAIN (COSTS OFF) SELECT * FROM mvtest_replace WHERE b = 1;
+SELECT * FROM mvtest_replace WHERE b = 1;
+RESET enable_seqscan;
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 8 AS a, 1 AS b;
+SET enable_seqscan = off; -- force index scan
+EXPLAIN (COSTS OFF) SELECT * FROM mvtest_replace WHERE b = 1;
+SELECT * FROM mvtest_replace WHERE b = 1;
+RESET enable_seqscan;
+
+-- Cannot change column data type
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 9 AS a, 'x' AS b; -- error
+SELECT * FROM mvtest_replace;
+
+-- Cannot rename column
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 10 AS a, 1 AS b2; -- error
+SELECT * FROM mvtest_replace;
+
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 11 AS a, 1 AS b, 'y' COLLATE "C" AS c;
+SELECT * FROM mvtest_replace;
+
+-- Cannot change column collation
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 12 AS a, 1 AS b, 'x' COLLATE "POSIX" AS c; -- error
+SELECT * FROM mvtest_replace;
+
+-- Cannot drop column
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 13 AS a, 1 AS b; -- error
+SELECT * FROM mvtest_replace;
+
+-- Must target a matview
+CREATE VIEW mvtest_not_mv AS
+  SELECT 1 AS a;
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_not_mv AS
+  SELECT 1 AS a; -- error
+DROP VIEW mvtest_not_mv;
+
+-- Cannot use OR REPLACE with IF NOT EXISTS
+CREATE OR REPLACE MATERIALIZED VIEW IF NOT EXISTS mvtest_replace AS
+  SELECT 1 AS a;
+
+DROP MATERIALIZED VIEW mvtest_replace;
+
+-- Clause WITH OLD DATA is not allowed when creating a new matview
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 17 AS a
+  WITH OLD DATA; -- error
+
+CREATE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 1 AS c1, 2 AS c2, 3 AS c3, 4 AS c4,
+         5 AS c5, 6 AS c6, 7 AS c7, null AS c8;
+-- Add a ninth column (exceeding the old t_bits)
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 1 AS c1, 2 AS c2, 3 AS c3, 4 AS c4,
+         5 AS c5, 6 AS c6, 7 AS c7, null AS c8,
+         null AS c9
+  WITH OLD DATA;
+SELECT c9 FROM mvtest_replace;
+
+-- Test constraint violation on WITH OLD DATA
+DROP MATERIALIZED VIEW mvtest_replace;
+CREATE DOMAIN mvtest_dom AS int
+  CONSTRAINT mvtest_dom_nn NOT NULL;
+CREATE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 1::mvtest_dom AS a;
+CREATE OR REPLACE MATERIALIZED VIEW mvtest_replace AS
+  SELECT 2::mvtest_dom AS a, 3::mvtest_dom AS b
+  WITH OLD DATA; -- error: new column "b" cannot be null
+SELECT a FROM mvtest_replace;
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index c5db6ca6705..2223a658899 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -3471,6 +3471,7 @@ WindowStatePerFunc
 WindowStatePerFuncData
 WithCheckOption
 WithClause
+WithDataOption
 WordBoundaryNext
 WordEntry
 WordEntryIN
-- 
2.54.0

^ permalink  raw  reply  [nested|flat] 29+ messages in thread

* Re: CREATE OR REPLACE MATERIALIZED VIEW
@ 2026-06-28 18:32  Erik Wienhold <ewie@ewie.name>
  parent: Soumya S Murali <soumyamurali.work@gmail.com>
  0 siblings, 1 reply; 29+ messages in thread

From: Erik Wienhold @ 2026-06-28 18:32 UTC (permalink / raw)
  To: Soumya S Murali <soumyamurali.work@gmail.com>; +Cc: Tom Lane <tgl@sss.pgh.pa.us>; Said Assemlal <sassemlal@neurorx.com>; pgsql-hackers

On 2026-05-01 17:59 +0200, Soumya S Murali wrote:
> I tested the patches and the feature is working as expected, I am able
> to create and replace a materialized view successfully and the data
> updates correctly after replacement. Replacing multiple times also
> works without any issues. Also I observed that adding new columns
> during replacement works fine but removing existing columns is not
> allowed, and it throws an error. This behavior seems correct and is
> similar to how normal views work. In cases where dependencies exist,
> the system prevents unsafe changes through these validations. Also,
> REFRESH MATERIALIZED VIEW works properly after replacement.

Thanks for testing!

> But a few limitations (if I am not wrong) are noticed. I am attaching
> the outputs here:-
> 1. CREATE OR REPLACE MATERIALIZED VIEW mv_test AS SELECT
> generate_series(1,5) AS a WITH NO DATA;) is not currently supported
> with the replace option and results in an error during validation.
> postgres=# CREATE OR REPLACE MATERIALIZED VIEW mv_test AS
> SELECT generate_series(1,5) AS a WITH NO DATA;
> ERROR:  cannot drop columns from materialized view
> 
> 2. When a dependent materialized view exists, replacing the base view
> fails due to column validation errors.
> postgres=# CREATE MATERIALIZED VIEW mv_table AS SELECT * FROM mv_test;
> SELECT 1
> postgres=# CREATE OR REPLACE MATERIALIZED VIEW mv_test AS SELECT 100 AS a;
> ERROR:  cannot drop columns from materialized view
> Here I think the system is not handling dependencies directly instead
> blocking changes indirectly due to column mismatch.
> 
> 3. Schema evolution is limited (cannot drop or modify existing columns).
> postgres=# CREATE OR REPLACE MATERIALIZED VIEW mv_test AS SELECT 10 AS a;
> ERROR:  cannot drop columns from materialized view

Please share your test script.  It looks like you're operating on an
existing mv_test with additional columns, probably from earlier test
cases.  And as you've noticed in your first paragraph, dropping columns
is not supported via CREATE OR REPLACE.  So your third point isn't even
valid.  Unless you mean "changing column values" when writing "modify
existing columns".  Changing the SELECT values should of course work if
the types are compatible with any existing column type.

-- 
Erik Wienhold





^ permalink  raw  reply  [nested|flat] 29+ messages in thread

* Re: CREATE OR REPLACE MATERIALIZED VIEW
@ 2026-07-23 07:05  Rafia Sabih <rafia.pghackers@gmail.com>
  parent: Erik Wienhold <ewie@ewie.name>
  0 siblings, 0 replies; 29+ messages in thread

From: Rafia Sabih @ 2026-07-23 07:05 UTC (permalink / raw)
  To: Erik Wienhold <ewie@ewie.name>; +Cc: Soumya S Murali <soumyamurali.work@gmail.com>; Tom Lane <tgl@sss.pgh.pa.us>; Said Assemlal <sassemlal@neurorx.com>; pgsql-hackers

On Mon, 29 Jun 2026 at 00:02, Erik Wienhold <ewie@ewie.name> wrote:

> On 2026-05-01 17:59 +0200, Soumya S Murali wrote:
> > I tested the patches and the feature is working as expected, I am able
> > to create and replace a materialized view successfully and the data
> > updates correctly after replacement. Replacing multiple times also
> > works without any issues. Also I observed that adding new columns
> > during replacement works fine but removing existing columns is not
> > allowed, and it throws an error. This behavior seems correct and is
> > similar to how normal views work. In cases where dependencies exist,
> > the system prevents unsafe changes through these validations. Also,
> > REFRESH MATERIALIZED VIEW works properly after replacement.
>
> Thanks for testing!
>
> > But a few limitations (if I am not wrong) are noticed. I am attaching
> > the outputs here:-
> > 1. CREATE OR REPLACE MATERIALIZED VIEW mv_test AS SELECT
> > generate_series(1,5) AS a WITH NO DATA;) is not currently supported
> > with the replace option and results in an error during validation.
> > postgres=# CREATE OR REPLACE MATERIALIZED VIEW mv_test AS
> > SELECT generate_series(1,5) AS a WITH NO DATA;
> > ERROR:  cannot drop columns from materialized view
> >
> > 2. When a dependent materialized view exists, replacing the base view
> > fails due to column validation errors.
> > postgres=# CREATE MATERIALIZED VIEW mv_table AS SELECT * FROM mv_test;
> > SELECT 1
> > postgres=# CREATE OR REPLACE MATERIALIZED VIEW mv_test AS SELECT 100 AS
> a;
> > ERROR:  cannot drop columns from materialized view
> > Here I think the system is not handling dependencies directly instead
> > blocking changes indirectly due to column mismatch.
> >
> > 3. Schema evolution is limited (cannot drop or modify existing columns).
> > postgres=# CREATE OR REPLACE MATERIALIZED VIEW mv_test AS SELECT 10 AS a;
> > ERROR:  cannot drop columns from materialized view
>
> Please share your test script.  It looks like you're operating on an
> existing mv_test with additional columns, probably from earlier test
> cases.  And as you've noticed in your first paragraph, dropping columns
> is not supported via CREATE OR REPLACE.  So your third point isn't even
> valid.  Unless you mean "changing column values" when writing "modify
> existing columns".  Changing the SELECT values should of course work if
> the types are compatible with any existing column type.
>
> --
> Erik Wienhold
>
>
> I looked into the latest version of this patch and there is an issue I am
seeing,
in CreateTableAsRelReplaceable when we are calling
checkMembershipInCurrentExtension() it is based on oldrelid, however
further in the code when we are finally calling create_ctas_replace it is
with matviewoid which is new oid which is is never checked against
checkMembershipInCurrentExtension, so couldn't there be a possibility that
in between these two calls a concurrent session drops and recreates the
matview of same name. Also, since there is no lock on the relation. Maybe
this is a pathological case, but wouldn't it be better to have something
such that check and replace are close together?

Another thing, there is a branch if (!into->replace)
in CreateTableAsRelReplaceable, however CreateTableAsRelReplaceable is
called only under if (is_matview && into->replace) in ExecCreateTableAs, so
that is dead code precisely.
-- 
Regards,
Rafia Sabih
CYBERTEC PostgreSQL International GmbH

^ permalink  raw  reply  [nested|flat] 29+ messages in thread

* Re: CREATE OR REPLACE MATERIALIZED VIEW
@ 2026-08-05 19:17  Paul A Jungwirth <pj@illuminatedcomputing.com>
  parent: Erik Wienhold <ewie@ewie.name>
  0 siblings, 1 reply; 29+ messages in thread

From: Paul A Jungwirth @ 2026-08-05 19:17 UTC (permalink / raw)
  To: Erik Wienhold <ewie@ewie.name>; +Cc: Tom Lane <tgl@sss.pgh.pa.us>; Said Assemlal <sassemlal@neurorx.com>; pgsql-hackers@postgresql.org, Haibo Yan <haibo.yan@hotmail.com>

Warda Bibi and Khoa Nguyen and I reviewed the v8 patch. I believe they
will share some of their own thoughts as well, but I wanted to send
what I have now:

On Sun, Jun 28, 2026 at 11:14 AM Erik Wienhold <ewie@ewie.name> wrote:
>
> On 2026-02-03 01:19 +0100, Paul A Jungwirth wrote:
> > ```
> > @@ -402,14 +529,15 @@ CreateTableAsRelExists(CreateTableAsStmt *ctas)
> >     oldrelid = get_relname_relid(into->rel->relname, nspid);
> >     if (OidIsValid(oldrelid))
> >     {
> > -       if (!ctas->if_not_exists)
> > +       if (!ctas->if_not_exists && !into->replace)
> > ```
> >
> > Changing the contract without changing the function comment seems wrong.
> >
> > But is this really factored correctly? Maybe the check should happen
> > in the caller instead.
> >
> > Does this require a change to ExplainOneUtility, which also calls this
> > function? At least we probably need to handle OR REPLACE there.
>
> I added the static CreateTableAsRelReplaceable to leave the existing
> CreateTableAsRelExists untouched.  The former now only checks whether
> the relation exists and can be replaced.  The logic in ExecCreateTableAs
> is changed to first check is_matview && into->replace.

This looks better to me. But I agree with Rafia's concern about
calling checkMembershipInCurrentExtension in both
CreateTableAsRelReplaceable and create_ctas_replace, and there being a
race condition.

Also I think you skipped our question about EXPLAIN here. In the OR
REPLACE case, we get a failure:

[v20devel:15432][2542702] postgres=# CREATE OR REPLACE MATERIALIZED
VIEW mx AS SELECT 2 AS a;
SELECT 1
Time: 3.429 ms
[v20devel:15432][2542702] postgres=# EXPLAIN CREATE OR REPLACE
MATERIALIZED VIEW mx AS SELECT 2 AS a;
ERROR:  relation "mx" already exists

> > ```
> >     if (newdesc->natts < olddesc->natts)
> > -       ereport(ERROR,
> > -               (errcode(ERRCODE_INVALID_TABLE_DEFINITION),
> > -                errmsg("cannot drop columns from view")));
> > +   {
> > +       if (is_matview)
> > +           ereport(ERROR,
> > +                   errcode(ERRCODE_INVALID_TABLE_DEFINITION),
> > +                   errmsg("cannot drop columns from materialized view"));
> > +       else
> > +           ereport(ERROR,
> > +                   (errcode(ERRCODE_INVALID_TABLE_DEFINITION),
> > +                    errmsg("cannot drop columns from view")));
> > +   }
> > ```
> >
> > Instead of repeating so much, perhaps `errmsg("cannot drop columns
> > from %s", is_matview ? "materialized view" : "view")`? Or since that
> > is probably bad for translators, at least this:
> >
> > ```
> > errmsg(is_matview
> >        ? "cannot drop columns from materialized view"
> >        : "cannot drop columns from view")
> > ```
> >
> > Likewise with more error messages in this function. (There are a lot of them.)
>
> When formatting it like errmsg(is_matview ? "" : "") the PO files only
> pick up the first of the two messages.  Pulling out the ternary like
> this would work:
>
>         is_matview ? errmsg(...) : errmsg(...)
>
> But instead I added function checkMatviewColumns to matview.c based on
> checkViewColumns to avoid the is_matview flag altogether.  Both
> functions still implement the same checks.  The very last comment in
> checkViewColumns mentions leaving column defaults in place.  Since
> default values can't be set on matview columns I figured that it's best
> to provide these checks as separate functions to avoid confusion.

It seems like quite a lot of duplication between checkViewColumns and
checkMatviewColumns. I think having a flag is better here. Maybe
someone more experienced can weigh in about `errmsg("cannot drop
columns from %s", is_matview ? "materialized view" : "view")`. That
seems like the best option to me, but `is_matview ? errmsg(...) :
errmsg(...)` would be okay too.

> > ```
> > diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
> > index 7aaf0e37ad8..e51ce2701be 100644
> > --- a/src/backend/parser/gram.y
> > +++ b/src/backend/parser/gram.y
> > @@ -4944,6 +4944,22 @@ CreateMatViewStmt:
> >                     $7->replace = true;
> >                     $$ = (Node *) ctas;
> >                 }
> > +       | CREATE OR REPLACE OptNoLog MATERIALIZED VIEW
> > create_mv_target AS SelectStmt WITH OLD DATA_P
> > +               {
> > +                   CreateTableAsStmt *ctas = makeNode(CreateTableAsStmt);
> > +
> > +                   ctas->query = $9;
> > +                   ctas->into = $7;
> > +                   ctas->objtype = OBJECT_MATVIEW;
> > +                   ctas->is_select_into = false;
> > +                   ctas->if_not_exists = false;
> > +                   /* cram additional flags into the IntoClause */
> > +                   $7->rel->relpersistence = $4;
> > +                   $7->skipData = false;
> > +                   $7->keepData = true;
> > +                   $7->replace = true;
> > +                   $$ = (Node *) ctas;
> > +               }
> >         ;
> > ```
> >
> > This is the fourth production for minor variations of
> > CreateMatViewStmt, which seems like a lot. Perhaps instead of
> > opt_with_data we add a new production opt_with_no_or_old_data that can
> > return 3 alteratives (WITH [{NO|OLD}] DATA)?
>
> I've added production opt_with_no_or_old_data as suggested, backed by
> the new enum WithDataOption in parsenodes.h.  With that, I replaced
> .keepData from v7 with just .data holding that new enum.
>
> Field .skipData is still in place but I think it should be migrated to
> that new .data field.  I haven't done that in v8, though, to keep the
> patch focused.

Using opt_with_no_or_old_data seems like an improvement. I agree about
consolidating .skipData and .data, which seem to overlap. And the new
.data field is not set in any production except the new one, so
elsewhere it is wrong.

Also Claude Code found several new issues, which we want to pass on:

This causes a segfault:

```
CREATE FUNCTION noop_et() RETURNS event_trigger LANGUAGE plpgsql AS
  $$ BEGIN RAISE NOTICE 'et fired: %', tg_tag; END $$;
CREATE EVENT TRIGGER et_end ON ddl_command_end EXECUTE FUNCTION noop_et();

CREATE MATERIALIZED VIEW m1 AS SELECT 1 AS a;
CREATE OR REPLACE MATERIALIZED VIEW m1 AS SELECT 2 AS a;   -- crash
```

We should add it to the event_trigger regress suite.

Here is a backtrace:

```
#0  EventTriggerAlterTableRelid (objectId=16386) at event_trigger.c:1803
        currentEventTriggerState->currentCommand->d.alterTable.objectId
= objectId;
#1  AlterTableInternal (relid=16386, ...)          at tablecmds.c:4662
#2  create_ctas_replace (..., matviewOid=16386)    at createas.c:341
#3  create_ctas_nodata (...)                       at createas.c:228
#4  ExecCreateTableAs (...)                        at createas.c:388
#5  ProcessUtilitySlow (...)                       at utility.c:1682
```

The problem is that in create_ctas_replace you have:

    /* EventTriggerAlterTableStart called by ProcessUtilitySlow */
    AlterTableInternal(matviewOid, atcmds, true);

But that comment isn't true. I think it's just copied from
DefineVirtualRelation? If you look in ProcessUtilitySlow, you can see
that T_ViewStmt does call EventTriggerAlterTable{Start,End}, but
T_CreateTableAsStmt doesn't.

But I don't think adding it is the right answer. For one thing, adding
it to ProcessUtilitySlow would call it for *all* CREATE TABLE AS
commands, and that would be a behavior change. More important, we
don't fire event triggers for purely internal ALTER TABLE commands.
You can see that T_RefreshMatViewStmt uses
EventTriggerInhibitCommandCollection to avoid them for the same
reason. I think that's the pattern we should follow.

It seems odd that we have these different command tags depending on
how you word the command:

```
CREATE MATERIALIZED VIEW m2 AS SELECT 1 AS a;
-> SELECT 1
CREATE OR REPLACE MATERIALIZED VIEW m2 AS SELECT 2 AS a;
-> REFRESH MATERIALIZED VIEW
CREATE OR REPLACE MATERIALIZED VIEW m2 AS SELECT 3 AS a WITH NO DATA;
-> REFRESH MATERIALIZED VIEW
CREATE OR REPLACE MATERIALIZED VIEW m2 AS SELECT 4 AS a WITH OLD DATA;
-> CREATE MATERIALIZED VIEW
```

The existing behavior is:

```
CREATE MATERIALIZED VIEW mt AS SELECT ...              -> SELECT 3
CREATE MATERIALIZED VIEW mt AS SELECT ... WITH NO DATA -> CREATE
MATERIALIZED VIEW
```

I think replacing should give the same command tag as creating. So perhaps this:

```
CREATE MATERIALIZED VIEW m2 AS SELECT 1 AS a;
-> SELECT 1
CREATE OR REPLACE MATERIALIZED VIEW m2 AS SELECT 2 AS a;
-> SELECT 1
CREATE OR REPLACE MATERIALIZED VIEW m2 AS SELECT 3 AS a WITH NO DATA;
-> CREATE MATERIALIZED VIEW
CREATE OR REPLACE MATERIALIZED VIEW m2 AS SELECT 4 AS a WITH OLD DATA;
-> CREATE MATERIALIZED VIEW
```

The last one is hard to say, but WITH OLD DATA avoids writing new heap
files, just like WITH NO DATA, so it seems correct.

In the regress tests you can test this by adding `\set QUIET false`
(as in triggers.sql and for_portion_of.sql).

There are some issues with Tom's rule that OR REPLACE should give the
same result as if you said CREATE. Potentially moving the tablespace
(because the default differs from the old matview's value) and causing
a table rewrite is a big effect, so let's at least document that it
might happen. More seriously, this can leave your indexes stranded in
the old tablespace, different from the matview's. That's not a state
you can normally achieve, so it seems risky for us and confusing for
users. I'm sure we don't want to sign up for handling that case going
forward. So if the tablespace changes, should we rewrite the indexes
too?

CreateTableAsRelReplaceable is almost identical to
CreateTableAsRelExists. And we can't actually reach the ereport, can
we? And I don't think we want to raise that notice anyway. With CREATE
OR REPLACE FUNCTION we don't say "already exists"; we just replace it.
Maybe that could be an Assert or at least an elog?

The new tests don't drop mvtest_replace and mvtest_dom. Sometimes we
do that to test pg_upgrade, but I don't think they actually add
anything there, so they should get cleaned up.

The prototype for create_ctas_replace names its first parameter tlist,
but the definition has attrList.

```
@@ -232,8 +376,37 @@ ExecCreateTableAs(ParseState *pstate,
CreateTableAsStmt *stmt,
     DestReceiver *dest;
     ObjectAddress address;

-    /* Check if the relation exists or not */
-    if (CreateTableAsRelExists(stmt))
+    /*
+     * Check if the relation exists or not.  An existing materialized view can
+     * be replaced.
+     */
+    if (is_matview && into->replace)
+    {
+        if (CreateTableAsRelReplaceable(stmt))
+        {
+            /* Change the relation to match the new query and other options. */
+            address = create_ctas_nodata(query->targetList, into);
+
+            /*
+             * Refresh the materialized view with a fake statement unless we
+             * must keep the old data.
+             */
+            if (into->data != WITHDATA_OLD)
+            {
+                RefreshMatViewStmt *refresh;
+
+                refresh = makeNode(RefreshMatViewStmt);
+                refresh->relation = into->rel;
+                refresh->skipData = into->skipData;
+                refresh->concurrent = false;
+
+                address = ExecRefreshMatView(refresh,
pstate->p_sourcetext, qc);
+            }
+
+            return address;
+        }
+    }
+    else if (CreateTableAsRelExists(stmt))
         return InvalidObjectAddress;

     /*
```

This return fails to call the post_parse_analyze_hook.

Should we add a CREATE OR REPLACE example to test_ddl_deparse?

We've tried to summarize and interpret Claude's findings as best we
can, but we've attached the raw Claude report as well if you're
interested. You can see that we didn't always agree 100%. Also it has
some suggested fixes for some of the problems it found, so that might
save you some time.

Yours,

--
Paul              ~{:-)
pj@illuminatedcomputing.com

Attachments:

  [text/markdown] create-or-replace-matview-review.md (25.7K, ../../CA+renyWJgjatyvJEKr9_GSnr3xr-sqLfhLR+rvw3MSygowR9xA@mail.gmail.com/2-create-or-replace-matview-review.md)
  download | inline:
# Review: `dba318893f8` — Add OR REPLACE option to CREATE MATERIALIZED VIEW

**Author:** Erik Wienhold <ewie@ewie.name>
**Branch:** `create-or-replace-matview` (on top of `dd62a71013c`)
**Diff:** 12 files, +813 / −27

---

## Summary

The feature is a genuinely useful one and the overall shape is right: reuse the
relation, run the column compatibility check that `CREATE OR REPLACE VIEW`
already does, drive the metadata changes through `AlterTableInternal()`, then
`StoreViewQuery()` the new rule. Doing it this way preserves the matview's OID,
ACLs, comments, indexes, and dependents, which is the whole point of `OR
REPLACE`. `WITH OLD DATA` is a sensible addition and the motivation (staying
populated so a later `REFRESH ... CONCURRENTLY` is legal) is real.

But it is not committable as-is. There is **one reproducible server crash**, one
**user-visible protocol regression** (wrong command tag), and a set of silent
side effects on matview storage properties that are neither documented nor, in
my opinion, defensible as designed.

**Verdict: needs another round.** Items 1 and 2 are blockers. Item 3 turns out to
be settled thread policy, not an open question — see §3 and §10, where I withdraw
my original recommendation.

Everything below was verified against an assert-enabled build of this commit
(`gcc 14.2.0`, `USE_ASSERT_CHECKING`). `make -C src/test/regress check` passes on
the unmodified commit: **245/245** — which is itself part of the problem, since
nothing exercises the crash in §1.

**Items 1 and 2 now have tests and fixes in the working tree** (§11).

---

## 1. Blocker: server crash when a `ddl_command_end` event trigger exists — *fixed, §11*

`create_ctas_replace()` calls `AlterTableInternal()` at
`src/backend/commands/createas.c:341`, under this comment:

```c
/* EventTriggerAlterTableStart called by ProcessUtilitySlow */
AlterTableInternal(matviewOid, atcmds, true);
```

The comment was copied from `src/backend/commands/view.c:164,197`, where it is
true — `ProcessUtilitySlow()` wraps `T_ViewStmt` in
`EventTriggerAlterTableStart()` / `EventTriggerAlterTableEnd()`
(`src/backend/tcop/utility.c:1649-1657`). It is **not** true for
`T_CreateTableAsStmt`, which has no such wrapper
(`src/backend/tcop/utility.c:1681-1684`). So
`currentEventTriggerState->currentCommand` is still NULL when
`AlterTableInternal()` dereferences it.

Reproducer:

```sql
CREATE FUNCTION noop_et() RETURNS event_trigger LANGUAGE plpgsql AS
  $$ BEGIN RAISE NOTICE 'et fired: %', tg_tag; END $$;
CREATE EVENT TRIGGER et_end ON ddl_command_end EXECUTE FUNCTION noop_et();

CREATE MATERIALIZED VIEW m1 AS SELECT 1 AS a;
CREATE OR REPLACE MATERIALIZED VIEW m1 AS SELECT 2 AS a;   -- crash
```

```
LOG:  client backend (PID 2335482) was terminated by signal 11: Segmentation fault
DETAIL:  Failed process was running: CREATE OR REPLACE MATERIALIZED VIEW m1 AS SELECT 2 AS a;
```

Backtrace:

```
#0  EventTriggerAlterTableRelid (objectId=16386) at event_trigger.c:1803
        currentEventTriggerState->currentCommand->d.alterTable.objectId = objectId;
#1  AlterTableInternal (relid=16386, ...)          at tablecmds.c:4662
#2  create_ctas_replace (..., matviewOid=16386)    at createas.c:341
#3  create_ctas_nodata (...)                       at createas.c:228
#4  ExecCreateTableAs (...)                        at createas.c:388
#5  ProcessUtilitySlow (...)                       at utility.c:1682
```

A trigger on `ddl_command_start` alone does not crash — it is specifically the
command-collection state set up for `ddl_command_end` / `sql_drop` /
`table_rewrite`.

**Fix direction:** wrap the `T_CreateTableAsStmt` case in `ProcessUtilitySlow()`
the same way `T_ViewStmt` is wrapped, or call
`EventTriggerAlterTableStart()`/`End()` around the `AlterTableInternal()` call in
`create_ctas_replace()` itself. The first is more consistent with existing
practice, but it changes event-trigger output for *all* CTAS, so it needs a
deliberate decision about what `pg_event_trigger_ddl_commands()` should report
for a replaced matview — right now it would surface a stashed `alter table`
subcommand list for something the user typed as `CREATE MATERIALIZED VIEW`.
Please also decide whether the internal `ALTER TABLE` bookkeeping should be
hidden from event triggers entirely, the way `ExecRefreshMatView()`'s internal
DDL is (`utility.c:1686-1705` inhibits collection around it).

Whatever the resolution: **`src/test/regress/sql/event_trigger.sql` must cover
`CREATE OR REPLACE MATERIALIZED VIEW`.** The full regression suite passes today
precisely because nothing exercises this combination.

---

## 2. Blocker: wrong command completion tag — *fixed, §11*

`ExecCreateTableAs()` hands off to `ExecRefreshMatView()` at
`src/backend/commands/createas.c:405` and lets it write the `QueryCompletion`.
The result is three different tags for what is nominally one command:

```
CREATE MATERIALIZED VIEW m2 AS SELECT 1 AS a;                          -> SELECT 1
CREATE OR REPLACE MATERIALIZED VIEW m2 AS SELECT 2 AS a;               -> REFRESH MATERIALIZED VIEW
CREATE OR REPLACE MATERIALIZED VIEW m2 AS SELECT 3 AS a WITH NO DATA;  -> REFRESH MATERIALIZED VIEW
CREATE OR REPLACE MATERIALIZED VIEW m2 AS SELECT 4 AS a WITH OLD DATA; -> CREATE MATERIALIZED VIEW
```

Reporting `REFRESH MATERIALIZED VIEW` for a statement the client sent as
`CREATE OR REPLACE MATERIALIZED VIEW` is a wire-protocol lie; drivers and
poolers that key off the tag will get it wrong. (The `WITH OLD DATA` case only
looks right by accident — nothing sets `qc` at all there, so it falls back to
`CreateCommandTag()`.)

The existing `SELECT n` tag for plain `CREATE MATERIALIZED VIEW` is
long-standing behavior and shouldn't change here, but the new paths need to
produce something consistent and deliberate. At minimum, pass a scratch
`QueryCompletion` to `ExecRefreshMatView()` and discard it, then set the tag
yourself.

Related: `address = ExecRefreshMatView(...)` at line 405 also overwrites the
address returned by `create_ctas_nodata()`. They happen to be the same relation
today, but the overwrite is gratuitous and obscures that.

---

## 3. Replacing a matview silently resets and rewrites its storage

> **Revised after reading the thread.** My original recommendation here — that
> unspecified clauses should preserve the existing access method, tablespace and
> reloptions — is **wrong**, and I withdraw it. Tom Lane ruled the opposite way
> on 2025-04-05, and the current code is the deliberate result of that feedback
> (v7/v8 changed *from* preserve *to* reset). See §10. What survives is the
> documentation gap and the stranded-index problem, neither of which the thread
> touches.

`create_ctas_replace()` unconditionally issues `AT_SetAccessMethod`,
`AT_SetTableSpace`, and `AT_ReplaceRelOptions`, filling in the *global defaults*
whenever the statement didn't name one (`createas.c:284-337`). The comment says
this is intentional — "the defaults apply as if they have not been specified at
all" — and it does mirror `CREATE OR REPLACE VIEW`'s treatment of reloptions.

The semantics are correct and settled. But they are **not documented at all**,
and they are more expensive and more surprising on a matview than on a view.

```sql
CREATE MATERIALIZED VIEW m7 WITH (autovacuum_enabled=false, fillfactor=70)
  TABLESPACE ts1 AS SELECT g AS a FROM generate_series(1,5) g;
--  reloptions                               | in_ts
--  {autovacuum_enabled=false,fillfactor=70} | t

CREATE OR REPLACE MATERIALIZED VIEW m7 AS
  SELECT g AS a FROM generate_series(1,99) g WITH OLD DATA;
--  reloptions | in_ts
--             | f
```

A user editing the query text of a matview, without mentioning storage at all,
gets their `autovacuum_enabled=false` dropped and their multi-terabyte matview
**physically relocated to the default tablespace**. For a view that costs
nothing; for a matview it is an unbounded amount of I/O nobody asked for.

It also leaves the relation internally inconsistent, because
`ALTER ... SET TABLESPACE` does not follow indexes:

```sql
CREATE MATERIALIZED VIEW m8 TABLESPACE ts1 AS SELECT g AS a FROM generate_series(1,3) g;
CREATE INDEX m8_idx ON m8(a) TABLESPACE ts1;
CREATE OR REPLACE MATERIALIZED VIEW m8 AS SELECT g AS a FROM generate_series(1,3) g;

--  relname | relkind |  tblspc
-- ---------+---------+-----------
--  m8      | m       | <default>     <- moved
--  m8_idx  | i       | ts1           <- left behind
```

What I'd still ask for, all of it compatible with the agreed semantics:

- **Document it.** `create_materialized_view.sgml` should state plainly that a
  bare `CREATE OR REPLACE MATERIALIZED VIEW` resets access method, tablespace,
  and storage parameters to their defaults, and that doing so can rewrite and
  relocate the entire matview. Right now a user has to read `createas.c` to find
  this out. Tom's contract argument is a good rationale and deserves to appear
  in the docs, not just the archives.
- **The stranded index is arguably a bug even under the contract.** Tom's rule
  is that C.O.R. produces the object definition you'd have gotten from plain
  `CREATE`. A plain `CREATE MATERIALIZED VIEW` has no indexes at all, so indexes
  are outside the contract — which means there is no principled reason for the
  replace to move the heap out from under them. Leaving a matview and its
  indexes in different tablespaces is a state the user cannot reach with any
  single command, and it silently doubles their tablespace footprint. Worth
  raising on-list; it may need `ATExecSetTableSpace`-style index handling, or an
  explicit decision that this is acceptable.
- The regression test only checks `pg_class.reltablespace` of the matview
  itself; it should check the indexes too, whatever the resolution.

---

## 4. Correctness: the existence check is done twice, and the first one is unlocked

`ExecCreateTableAs()` calls `CreateTableAsRelReplaceable()`
(`createas.c:385`), which resolves the name with a bare `get_relname_relid()`
and **no lock** (`createas.c:628`). The real, locked lookup happens later in
`create_ctas_nodata()` via `RangeVarGetAndCheckCreationNamespace(...,
AccessExclusiveLock, &matviewOid)` (`createas.c:221`).

Two windows follow from that:

- The matview is dropped concurrently between the two lookups. `matviewOid`
  comes back invalid, we silently fall into `create_ctas_internal()` and create
  a fresh matview — **including when `WITH OLD DATA` was specified**, which
  `ExecCreateTableAs()` is supposed to reject for a new matview
  (`createas.c:449`). That check is on the other branch, so it never fires. The
  user gets a matview marked populated with no rows and no error.
- The matview is created concurrently between the two lookups. We take the
  create path and fail with `relation "x" already exists` despite `OR REPLACE`.

Both are narrow, but they're avoidable: do the lookup once, under
`AccessExclusiveLock`, and branch on the result — which is how
`DefineVirtualRelation()` handles the same problem for views.

---

## 5. Code duplication

Three pieces of this patch are near-verbatim copies of existing code:

- **`checkMatviewColumns()`** (`matview.c:978`) vs. **`checkViewColumns()`**
  (`view.c:266`). These are the same function modulo the word "view" →
  "materialized view" in five error messages, and the copy carries over the
  `/* XXX msg not right ... */` comment verbatim. Please factor into one
  function parameterized by relkind (or by an error-message noun) rather than
  forking it.
- **`CreateTableAsRelReplaceable()`** (`createas.c:619`) vs.
  **`CreateTableAsRelExists()`** (`createas.c:571`). Identical except
  `ctas->if_not_exists` → `into->replace` and the trailing NOTICE. One function
  with a flag would do.
- The new `CreateMatViewStmt` grammar production (`gram.y:5089`) duplicates the
  existing one. That may be unavoidable given the `IF NOT EXISTS` interaction,
  but see below.

---

## 6. `IntoClause` now carries the same information twice

`gram.y:5100-5101`:

```c
$7->skipData = $10 == WITHDATA_NONE;
$7->data = $10;
```

`skipData` and `data` are redundant, and only the new `OR REPLACE` production
sets `data` at all. Every other producer of an `IntoClause` — plain
`CREATE MATERIALIZED VIEW` (`gram.y:5086`), `CREATE TABLE AS`
(`gram.y:5004,5018,5072`), `SELECT INTO` (`gram.y:5141`), `EXECUTE ... INTO`
(`gram.y:12954,12972`) — leaves `data` at its zero value, `WITHDATA_DEFAULT`.
So after `CREATE TABLE AS ... WITH NO DATA`, the node claims `data ==
WITHDATA_DEFAULT` while `skipData == true`. The field lies about the statement
in every path but one.

Nothing in this patch reads it incorrectly, but it is a trap for the next
person. Either replace `skipData` with the enum everywhere (my preference —
`WithDataOption` is a good abstraction and the mechanical churn is small), or
don't add the field and derive `WITH OLD DATA` some other way.

Minor, in the same area: `WITHDATA_DEFAULT` is a confusing name for "WITH DATA",
given it also means "clause omitted". `WITHDATA_YES` or `WITHDATA_DATA` reads
better against `WITHDATA_NONE` / `WITHDATA_OLD`.

---

## 7. Documentation

- The synopsis reads `CREATE [ OR REPLACE ] MATERIALIZED VIEW [ IF NOT EXISTS ]
  ...`, which implies the two are combinable. They are not — it's a hard syntax
  error from the grammar. The prose says so a few lines later, but the synopsis
  should show two forms, or at least not present the impossible one.
- Nothing documents what `OR REPLACE` *preserves* — the OID, owner, ACLs,
  comments, indexes, and dependent views. That's the main reason to use it over
  `DROP` + `CREATE`, and it should be stated.
- Nothing documents the storage-property reset described in §3.
- `WITH OLD DATA` should say explicitly that the retained rows are the output of
  the *old* query and may not match the new definition until a refresh, and that
  columns added by the replace are NULL-filled in the retained rows.
- "Replaces a materialized view if it already exists." — worth adding that the
  new query's output columns must match the old ones by name, type, typmod, and
  collation, and may only be added to at the end. That restriction is the first
  thing users will hit.

---

## 8. Test coverage

The new tests in `matview.sql` are good on the column-compatibility rules and
the `WITH OLD DATA` semantics — the ninth-column `t_bits` case and the domain
NOT NULL violation are nice catches. Gaps:

- **Event triggers** (§1) — a crash today.
- **Command tags** (§2) — nothing checks them.
- **Permissions.** Ownership *is* correctly enforced, via `ATSimplePermissions()`
  on the generated subcommands (verified: a non-owner with `CREATE` on the
  schema gets `ERROR: must be owner of materialized view mv`). But that's an
  incidental consequence of routing through `AlterTableInternal()`, not an
  explicit check — exactly the kind of thing that should be pinned by a test.
- **Index tablespace divergence** (§3).
- No isolation test for concurrent `CREATE OR REPLACE` / `DROP` / `REFRESH`
  against the same matview (§4).
- The new tests leave `mvtest_replace` and domain `mvtest_dom` behind. Harmless,
  and arguably useful for the `pg_upgrade` test, but it's a departure from the
  rest of the file — worth a word in the commit message if intentional.

Also, `-- Cannot use OR REPLACE with IF NOT EXISTS` has no `-- error` marker,
unlike the neighboring negative cases.

---

## 9. Smaller items

- `createas.c:68`: the prototype names the first parameter `tlist`; the
  definition at line 243 calls it `attrList`. The definition is right.
- `createas.c:631`: `if (!into->replace) ereport(ERROR, ...)` inside
  `CreateTableAsRelReplaceable()` is dead — the sole caller only enters when
  `into->replace` is true.
- `create_ctas_nodata()` now dispatches to the replace path, so its name and
  header comment no longer describe what it does. Either rename it or hoist the
  replace decision up into `ExecCreateTableAs()` (which would also fix §4).
- The replace path returns at `createas.c:407`, before the
  `JumbleQuery()` / `post_parse_analyze_hook` block at `createas.c:418-422`. So
  extensions hooking `post_parse_analyze` never see the CTAS query for a
  replace. I checked `pg_stat_statements` specifically and normalization still
  works (utility-statement jumbling covers it — `CREATE OR REPLACE MATERIALIZED
  VIEW m6 AS SELECT $1 AS a`, 2 calls), so there's no user-visible bug I can
  demonstrate. The asymmetry still looks unintentional.
- `parsenodes.h`: the patch deletes a blank line at the end of the file, which
  is unrelated to the feature. Drop that hunk.
- `tab-complete.in.c`: the completion arms are now paired
  `Matches(...) || Matches("CREATE", "OR", "REPLACE", ...)` five times over. If
  there's an existing idiom for optional `OR REPLACE` prefixes it'd read better;
  if not, this is acceptable, just verbose. `"MATERIALIZED VIEW"` is appended to
  the end of the `CREATE OR REPLACE` list rather than near `"VIEW"` — minor
  placement nit.
- No handling in `src/test/modules/test_ddl_deparse` — `matviews.sql` there
  covers `CREATE`/`REFRESH` only. Once §1 is resolved this will need attention.

---

## 10. Cross-check against the pgsql-hackers thread

Thread: <https://www.postgresql.org/message-id/flat/3c86a16f-4272-4df3-9959-70a9a7d88a71%40ewie.name>
(started 2024-07-02, last message Rafia Sabih 2026-07-23, still awaiting review).

**Already settled on-list — I was wrong:**

- **§3, storage-property reset.** Tom Lane, 2025-04-05:
  > "The contract for any form of C.O.R. is that it will either fail, or produce
  > exactly the same object definition that you would have gotten from plain
  > CREATE with no conflicting object. The v6 code is visibly not doing that for
  > properties such as tablespace --- if the command doesn't mention that, you
  > don't get the default tablespace, you get whatever the old object had."

  The current behavior is what he asked for. My "preserve the old values"
  suggestion is exactly the v6 behavior he rejected. Withdrawn.

- **`WITH OLD DATA` on a new matview.** Same message:
  > "BTW, I'm inclined to think that WITH OLD DATA ought to fail if the command
  > isn't replacing an existing matview. It seems inconsistent to silently
  > reinterpret it as WITH DATA [...]"

  Already implemented (`createas.c:449`). Note this makes the race in §4 a
  *correctness* problem, not just a tidiness one: losing the race silently
  re-enables the behavior Tom explicitly asked to be rejected.

- **`IF NOT EXISTS` deprecation.** Proposed as patch 0002, ruled a "nonstarter"
  by Tom, dropped by the author. Correctly absent from this commit.

**Already reported by others:**

- §4 (unlocked existence check / race) and the dead `if (!into->replace)` branch
  in §9 were both raised by Rafia Sabih on 2026-07-23 — the most recent message,
  and still open. My §4 adds the specific consequence that the `WITH OLD DATA`
  guard can be bypassed.
- Several §5/§6 points (the `checkMatviewColumns()` split, `WithDataOption`,
  `for_each_from`, the `create_ctas_replace()` extraction) came out of the
  2026-02-03 review round — i.e. this commit is already a response to them. The
  duplication I flag in §5 is what that refactoring left behind, so treat it as a
  follow-on suggestion rather than a new complaint.

**Not raised by anyone, including me until now:**

- **§1 (crash) and §2 (command tag) appear nowhere in the thread.** I searched it
  for event triggers, `ddl_command_end`, `pg_event_trigger_ddl_commands`, crashes,
  assertion failures, completion tags, `QueryCompletion`, and `RefreshMatViewByOid`
  — no hits. Two years of review, and neither has been looked at.
- §3's stranded-index consequence and the missing documentation of the reset
  semantics are likewise undiscussed. The thread argued about *what* the
  semantics should be and never came back to writing them down.

Worth noting for §2 that the fix is *implied* by Tom's own contract: plain
`CREATE MATERIALIZED VIEW` reports `SELECT n`, so under "produce exactly what
plain CREATE would" the replace must too. That argument is worth making on-list.

---

## 11. Tests and fixes (applied in the working tree)

Six files, +103/−11. Full suite green afterwards: regress **245/245**,
isolation **131/131**, `test_ddl_deparse` **22/22**.

### Test for §2 — `src/test/regress/sql/matview.sql`

Completion tags aren't visible under `pg_regress` because psql runs with `-q`.
`\set QUIET false` re-enables them, an idiom already used by `triggers.sql`,
`psql.sql`, and `for_portion_of.sql`. All four variants are covered.

Before the fix:

```
CREATE MATERIALIZED VIEW mvtest_tag AS ...           -> SELECT 3
CREATE OR REPLACE MATERIALIZED VIEW mvtest_tag AS ...-> REFRESH MATERIALIZED VIEW   <- wrong
  ... WITH NO DATA;                                  -> REFRESH MATERIALIZED VIEW   <- wrong
  ... WITH OLD DATA;                                 -> CREATE MATERIALIZED VIEW
```

After:

```
CREATE MATERIALIZED VIEW mvtest_tag AS ...           -> SELECT 3
CREATE OR REPLACE MATERIALIZED VIEW mvtest_tag AS ...-> SELECT 5
  ... WITH NO DATA;                                  -> CREATE MATERIALIZED VIEW
  ... WITH OLD DATA;                                 -> CREATE MATERIALIZED VIEW
```

The two `WITH ... DATA` forms now match plain
`CREATE MATERIALIZED VIEW ... WITH NO DATA`, which reports
`CREATE MATERIALIZED VIEW` because it runs no refresh at all.

### Test for §1 — `src/test/regress/sql/event_trigger.sql`

Added directly beneath the existing `-- View with column additions` case, under
the same `regress_event_trigger_report_end` trigger, so the matview path is
tested exactly where the view path already is. On the unfixed tree this test
takes the server down:

```
+CREATE OR REPLACE MATERIALIZED VIEW evttrig.one_matview AS
+  SELECT * FROM evttrig.two, evttrig.id;
+server closed the connection unexpectedly
```

### Fix for §2 — `src/backend/commands/createas.c`

The patch built a fake `RefreshMatViewStmt` and called `ExecRefreshMatView()`,
which hardcodes `is_create = false`. But master already grew
`RefreshMatViewByOid(..., bool is_create, ...)` for exactly this purpose —
`matview.c:388` picks `CMDTAG_SELECT` vs `CMDTAG_REFRESH_MATERIALIZED_VIEW` off
that flag, and the plain `CREATE MATERIALIZED VIEW` path at `createas.c:474`
already uses it. The replace path just needs to do the same:

```c
if (into->data == WITHDATA_DEFAULT)
    RefreshMatViewByOid(address.objectId, true, false, false,
                        pstate->p_sourcetext, qc);
else if (into->data == WITHDATA_NONE)
    RefreshMatViewByOid(address.objectId, true, true, false,
                        pstate->p_sourcetext, NULL);
/* For WITHDATA_OLD the existing contents are left untouched. */
```

`WITH NO DATA` passes `qc = NULL` so the tag falls back to `CreateCommandTag()`,
matching the plain path. This also drops the fake-statement hack and the
gratuitous overwrite of `address` noted in §2.

One thing to check on-list: `ExecRefreshMatView()` resolves the name through
`RangeVarCallbackMaintainsTable`, which permits the `MAINTAIN` privilege as well
as ownership. Entering by OID skips that. It is not a privilege loosening —
`ATSimplePermissions()` has already demanded full ownership by this point, which
is strictly stronger — but it is a behavior change worth stating rather than
letting a reviewer discover.

### Fix for §1 — `src/backend/tcop/utility.c`

Make the copied comment true rather than deleting it: wrap `T_CreateTableAsStmt`
the way `T_ViewStmt` is already wrapped, twelve cases further down.

```c
case T_CreateTableAsStmt:
    EventTriggerAlterTableStart(parsetree);
    address = ExecCreateTableAs(pstate, (CreateTableAsStmt *) parsetree,
                                params, queryEnv, qc);
    EventTriggerCollectSimpleCommand(address, secondaryObject, parsetree);
    commandCollected = true;
    EventTriggerAlterTableEnd();
    break;
```

This is safe for every other `CreateTableAsStmt` because
`EventTriggerAlterTableEnd()` discards the entry when no subcommands were
collected (`event_trigger.c:1862`), and `EventTriggerCollectSimpleCommand()` is
the same call the generic tail would have made. Verified: plain `CREATE TABLE AS`
and `SELECT INTO` produce unchanged event-trigger output, and no other expected
file moved.

Resulting event-trigger output mirrors the view case exactly — one notice for the
command, one for the stashed `ALTER TABLE`:

```
CREATE OR REPLACE MATERIALIZED VIEW evttrig.one_matview AS ...
NOTICE:  END: command_tag=CREATE MATERIALIZED VIEW type=materialized view identity=evttrig.one_matview
NOTICE:  END: command_tag=CREATE MATERIALIZED VIEW type=materialized view identity=evttrig.one_matview
```

An alternative worth considering: suppress the internal `ALTER TABLE` from
collection entirely, the way `ProcessUtilitySlow()` already inhibits collection
around `ExecRefreshMatView()` (`utility.c:1686-1705`). I chose consistency with
`CREATE OR REPLACE VIEW` instead, but this is a judgement call for the list.

---

## What I verified

| | |
|---|---|
| Build | clean, assert-enabled, no new warnings (before and after fixes) |
| `make -C src/test/regress check` | 245/245 on the commit; 245/245 after fixes |
| `make -C src/test/isolation check` | 131/131 after fixes |
| `make -C src/test/modules/test_ddl_deparse check` | 22/22 after fixes |
| New tests fail on unfixed tree | yes — crash in `event_trigger`, wrong tags in `matview` |
| Plain `CREATE TABLE AS` / `SELECT INTO` event-trigger output | unchanged by the `utility.c` fix |
| Crash (§1) | reproduced, core dumped, backtrace above |
| Command tags (§2) | reproduced, all four variants |
| Storage reset / index divergence (§3) | reproduced |
| Ownership enforcement | confirmed working |
| `CREATE OR REPLACE UNLOGGED MATERIALIZED VIEW` | correctly rejected (`analyze.c:3566`) |
| Replace with a dependent view / dependent matview | works |
| Replace targeting a table or a plain view | correctly rejected |
| Column alias list, `WITH DATA` / `WITH NO DATA` / `WITH OLD DATA` | behave as documented |
| Replace of an unpopulated matview with `WITH OLD DATA` | stays unpopulated, as expected |
| `pg_stat_statements` normalization | unaffected |

^ permalink  raw  reply  [nested|flat] 29+ messages in thread

* Re: CREATE OR REPLACE MATERIALIZED VIEW
@ 2026-08-14 16:28  Paul A Jungwirth <pj@illuminatedcomputing.com>
  parent: Paul A Jungwirth <pj@illuminatedcomputing.com>
  0 siblings, 1 reply; 29+ messages in thread

From: Paul A Jungwirth @ 2026-08-14 16:28 UTC (permalink / raw)
  To: Erik Wienhold <ewie@ewie.name>; +Cc: Tom Lane <tgl@sss.pgh.pa.us>; Said Assemlal <sassemlal@neurorx.com>; pgsql-hackers@postgresql.org, Haibo Yan <haibo.yan@hotmail.com>

On Wed, Aug 5, 2026 at 12:17 PM Paul A Jungwirth
<pj@illuminatedcomputing.com> wrote:
>
> There are some issues with Tom's rule that OR REPLACE should give the
> same result as if you said CREATE. Potentially moving the tablespace
> (because the default differs from the old matview's value) and causing
> a table rewrite is a big effect, so let's at least document that it
> might happen. More seriously, this can leave your indexes stranded in
> the old tablespace, different from the matview's. That's not a state
> you can normally achieve, so it seems risky for us and confusing for
> users. I'm sure we don't want to sign up for handling that case going
> forward. So if the tablespace changes, should we rewrite the indexes
> too?

Sorry, I don't know what I was thinking here: of course you can put an
index in a different tablespace than its table (or matview). Still, it
does possibly break Tom's principle that CREATE OR REPLACE should give
you the same result with or without a prior object. So if we are
moving the matview's heap, maybe we should move its indexes too. Or
maybe not: they have their own CREATE commands and are really separate
objects. Whatever we do, it seems worth documenting.

Yours,

-- 
Paul              ~{:-)
pj@illuminatedcomputing.com






^ permalink  raw  reply  [nested|flat] 29+ messages in thread

* Re: CREATE OR REPLACE MATERIALIZED VIEW
@ 2026-08-14 17:34  Robert Haas <robertmhaas@gmail.com>
  parent: Paul A Jungwirth <pj@illuminatedcomputing.com>
  0 siblings, 1 reply; 29+ messages in thread

From: Robert Haas @ 2026-08-14 17:34 UTC (permalink / raw)
  To: Paul A Jungwirth <pj@illuminatedcomputing.com>; +Cc: Erik Wienhold <ewie@ewie.name>; Tom Lane <tgl@sss.pgh.pa.us>; Said Assemlal <sassemlal@neurorx.com>; pgsql-hackers@postgresql.org, Haibo Yan <haibo.yan@hotmail.com>

Hi,

On Fri, Aug 14, 2026 at 12:28 PM Paul A Jungwirth
<pj@illuminatedcomputing.com> wrote:
> Sorry, I don't know what I was thinking here: of course you can put an
> index in a different tablespace than its table (or matview). Still, it
> does possibly break Tom's principle that CREATE OR REPLACE should give
> you the same result with or without a prior object. So if we are
> moving the matview's heap, maybe we should move its indexes too. Or
> maybe not: they have their own CREATE commands and are really separate
> objects. Whatever we do, it seems worth documenting.

I think CREATE OR REPLACE is the wrong syntax here. I think what
people would most often want to do is edit the query used to refresh
the query without disturbing the existing data, indexes, or other
ancillary properties of the view in any way, but just changing what
happens on next refresh. We have CREATE OR REPLACE FUNCTION because
replacing a function in its entirety is a sensible thing to do, but we
don't have CREATE OR REPLACE TABLE because tables contain data that
you're unlikely to want to throw out. Materialized views are more like
tables in this respect: they have data. Granted, it can be
regenerated, but that's probably expensive.

So I'd propose that this should be a form of ALTER MATERIALIZED VIEW,
and that it be defined as suggested above, just replacing the refresh
query.

I also agree with Tom's upthread comment that removing CREATE IF NOT
EXISTS is a nonstarter.

-- 
Robert Haas
EDB: http://www.enterprisedb.com





^ permalink  raw  reply  [nested|flat] 29+ messages in thread

* Re: CREATE OR REPLACE MATERIALIZED VIEW
@ 2026-08-14 18:05  Tom Lane <tgl@sss.pgh.pa.us>
  parent: Robert Haas <robertmhaas@gmail.com>
  0 siblings, 1 reply; 29+ messages in thread

From: Tom Lane @ 2026-08-14 18:05 UTC (permalink / raw)
  To: Robert Haas <robertmhaas@gmail.com>; +Cc: Paul A Jungwirth <pj@illuminatedcomputing.com>; Erik Wienhold <ewie@ewie.name>; Said Assemlal <sassemlal@neurorx.com>; pgsql-hackers@postgresql.org, Haibo Yan <haibo.yan@hotmail.com>

Robert Haas <robertmhaas@gmail.com> writes:
> I think CREATE OR REPLACE is the wrong syntax here. I think what
> people would most often want to do is edit the query used to refresh
> the query without disturbing the existing data, indexes, or other
> ancillary properties of the view in any way, but just changing what
> happens on next refresh. We have CREATE OR REPLACE FUNCTION because
> replacing a function in its entirety is a sensible thing to do, but we
> don't have CREATE OR REPLACE TABLE because tables contain data that
> you're unlikely to want to throw out. Materialized views are more like
> tables in this respect: they have data. Granted, it can be
> regenerated, but that's probably expensive.

> So I'd propose that this should be a form of ALTER MATERIALIZED VIEW,
> and that it be defined as suggested above, just replacing the refresh
> query.

This approach makes sense to me.  Note that this would imply that the
revised query has to still produce the same set of columns, else it's
inconsistent with the MV's stored data (but maybe we could allow
adding new columns at the end, as with tables).  I suppose this means
that if you want a different set of columns, you have no recourse
short of dropping the MV.

			regards, tom lane






^ permalink  raw  reply  [nested|flat] 29+ messages in thread

* Re: CREATE OR REPLACE MATERIALIZED VIEW
@ 2026-08-14 18:27  Isaac Morland <isaac.morland@gmail.com>
  parent: Tom Lane <tgl@sss.pgh.pa.us>
  0 siblings, 1 reply; 29+ messages in thread

From: Isaac Morland @ 2026-08-14 18:27 UTC (permalink / raw)
  To: Tom Lane <tgl@sss.pgh.pa.us>; +Cc: Robert Haas <robertmhaas@gmail.com>; Paul A Jungwirth <pj@illuminatedcomputing.com>; Erik Wienhold <ewie@ewie.name>; Said Assemlal <sassemlal@neurorx.com>; pgsql-hackers@postgresql.org, Haibo Yan <haibo.yan@hotmail.com>

On Fri, 14 Aug 2026 at 14:05, Tom Lane <tgl@sss.pgh.pa.us> wrote:

> Robert Haas <robertmhaas@gmail.com> writes:
> > I think CREATE OR REPLACE is the wrong syntax here. I think what
> > people would most often want to do is edit the query used to refresh
> > the query without disturbing the existing data, indexes, or other
> > ancillary properties of the view in any way, but just changing what
> > happens on next refresh. We have CREATE OR REPLACE FUNCTION because
> > replacing a function in its entirety is a sensible thing to do, but we
> > don't have CREATE OR REPLACE TABLE because tables contain data that
> > you're unlikely to want to throw out. Materialized views are more like
> > tables in this respect: they have data. Granted, it can be
> > regenerated, but that's probably expensive.
>
> > So I'd propose that this should be a form of ALTER MATERIALIZED VIEW,
> > and that it be defined as suggested above, just replacing the refresh
> > query.
>
> This approach makes sense to me.  Note that this would imply that the
> revised query has to still produce the same set of columns, else it's
> inconsistent with the MV's stored data (but maybe we could allow
> adding new columns at the end, as with tables).  I suppose this means
> that if you want a different set of columns, you have no recourse
> short of dropping the MV.
>

I think it's important that there be a single command that can create a new
MV if it doesn't exist, or update its base query if it does. I shouldn't
have to explicitly write code to say if it doesn't exist then create it,
otherwise alter it to change the base query.

This sounds like CREATE OR REPLACE to me, even if there is something to
explain in the documentation about what happens to the existing data
(assuming it is currently populated). I don't think of CREATE OR REPLACE as
completely re-creating an object anyhow. Certainly it can't arbitrarily
replace a function (can't change return type) nor a view (can't remove or
change type of a column). It's OK if not all possible changes are supported
by the "OR REPLACE" part of the command.

^ permalink  raw  reply  [nested|flat] 29+ messages in thread

* Re: CREATE OR REPLACE MATERIALIZED VIEW
@ 2026-08-14 19:01  Tom Lane <tgl@sss.pgh.pa.us>
  parent: Isaac Morland <isaac.morland@gmail.com>
  0 siblings, 2 replies; 29+ messages in thread

From: Tom Lane @ 2026-08-14 19:01 UTC (permalink / raw)
  To: Isaac Morland <isaac.morland@gmail.com>; +Cc: Robert Haas <robertmhaas@gmail.com>; Paul A Jungwirth <pj@illuminatedcomputing.com>; Erik Wienhold <ewie@ewie.name>; Said Assemlal <sassemlal@neurorx.com>; pgsql-hackers@postgresql.org, Haibo Yan <haibo.yan@hotmail.com>

Isaac Morland <isaac.morland@gmail.com> writes:
> This sounds like CREATE OR REPLACE to me, even if there is something to
> explain in the documentation about what happens to the existing data
> (assuming it is currently populated). I don't think of CREATE OR REPLACE as
> completely re-creating an object anyhow. Certainly it can't arbitrarily
> replace a function (can't change return type) nor a view (can't remove or
> change type of a column). It's OK if not all possible changes are supported
> by the "OR REPLACE" part of the command.

To my mind, the formal requirement for CREATE OR REPLACE should be
"if the command succeeds, the resulting object properties are
identical to what they'd be if we were creating it fresh" -- basically
a form of idempotency.  It's okay to fail when there are reasons why
we can't or shouldn't make that so.

In particular, ISTM that if we invent CREATE OR REPLACE MATERIALIZED
VIEW, then the view content should be either computed afresh or left
empty (depending on WITH NO DATA); it should never leave stale data.
But I think Robert is correct that an ALTER command that does keep
the old data is often going to be what's wanted.

			regards, tom lane






^ permalink  raw  reply  [nested|flat] 29+ messages in thread

* Re: CREATE OR REPLACE MATERIALIZED VIEW
@ 2026-08-21 12:30  Robert Haas <robertmhaas@gmail.com>
  parent: Tom Lane <tgl@sss.pgh.pa.us>
  1 sibling, 0 replies; 29+ messages in thread

From: Robert Haas @ 2026-08-21 12:30 UTC (permalink / raw)
  To: Tom Lane <tgl@sss.pgh.pa.us>; +Cc: Isaac Morland <isaac.morland@gmail.com>; Paul A Jungwirth <pj@illuminatedcomputing.com>; Erik Wienhold <ewie@ewie.name>; Said Assemlal <sassemlal@neurorx.com>; pgsql-hackers@postgresql.org, Haibo Yan <haibo.yan@hotmail.com>

On Fri, Aug 14, 2026 at 3:01 PM Tom Lane <tgl@sss.pgh.pa.us> wrote:
> To my mind, the formal requirement for CREATE OR REPLACE should be
> "if the command succeeds, the resulting object properties are
> identical to what they'd be if we were creating it fresh" -- basically
> a form of idempotency.  It's okay to fail when there are reasons why
> we can't or shouldn't make that so.

I agree. And I think that's going to make people sad here...

> In particular, ISTM that if we invent CREATE OR REPLACE MATERIALIZED
> VIEW, then the view content should be either computed afresh or left
> empty (depending on WITH NO DATA); it should never leave stale data.

...precisely because of this. If the materialized view already exists,
it's pretty questionable whether you actually want to overwrite random
properties of it like the tablespace or storage options because you
did CREATE OR REPLACE, but it's *really* unlikely that you want to
discard the data. And yet, by the definition of CREATE OR REPLACE,
that is exactly what should happen.

> But I think Robert is correct that an ALTER command that does keep
> the old data is often going to be what's wanted.

It's also worth keeping in mind here that if we never add CREATE OR
REPLACE, people can still get that behavior with BEGIN; DROP IF
EXISTS; CREATE; COMMIT. The major disadvantage of that compared to
CREATE OR REPLACE is that CREATE OR REPLACE can handle the case where
there are dependencies on the replaced object, and DROP IF EXISTS will
have to either fail or drop the dependent objects in that case. But
replacing the object without disturbing dependencies can only be made
to work anyway if the column list hasn't changed in incompatible ways,
which is a real case, but not the only one. And if you do have that
use case, I think there's a really good chance you're going to be
happier with ALTER MATERIALIZED VIEW ... <change the query>. That way,
you have a choice of whether to refresh afterwards or not, and you
don't accidentally reset any ancillary properties.

-- 
Robert Haas
EDB: http://www.enterprisedb.com





^ permalink  raw  reply  [nested|flat] 29+ messages in thread

* Re: CREATE OR REPLACE MATERIALIZED VIEW
@ 2026-08-21 13:13  Isaac Morland <isaac.morland@gmail.com>
  parent: Tom Lane <tgl@sss.pgh.pa.us>
  1 sibling, 0 replies; 29+ messages in thread

From: Isaac Morland @ 2026-08-21 13:13 UTC (permalink / raw)
  To: Tom Lane <tgl@sss.pgh.pa.us>; +Cc: Robert Haas <robertmhaas@gmail.com>; Paul A Jungwirth <pj@illuminatedcomputing.com>; Erik Wienhold <ewie@ewie.name>; Said Assemlal <sassemlal@neurorx.com>; pgsql-hackers@postgresql.org, Haibo Yan <haibo.yan@hotmail.com>

On Fri, 14 Aug 2026 at 15:01, Tom Lane <tgl@sss.pgh.pa.us> wrote:

> Isaac Morland <isaac.morland@gmail.com> writes:
> > This sounds like CREATE OR REPLACE to me, even if there is something to
> > explain in the documentation about what happens to the existing data
> > (assuming it is currently populated). I don't think of CREATE OR REPLACE
> as
> > completely re-creating an object anyhow. Certainly it can't arbitrarily
> > replace a function (can't change return type) nor a view (can't remove or
> > change type of a column). It's OK if not all possible changes are
> supported
> > by the "OR REPLACE" part of the command.
>
> To my mind, the formal requirement for CREATE OR REPLACE should be
> "if the command succeeds, the resulting object properties are
> identical to what they'd be if we were creating it fresh" -- basically
> a form of idempotency.  It's okay to fail when there are reasons why
> we can't or shouldn't make that so.
>

Yes, I can see the logic of this.

On the other hand, what do we mean by "properties of the object"? If we
mean "what pg_dump would dump", I don't believe the data stored in a
materialized view affect what gets dumped.

Perhaps there could be an option — "WITH EXISTING DATA". I'm actually a bit
skeptical of how often this would be wanted. If I’ve updated the query that
defines the contents of the view, why would I still want the old data that
were computed used the old query? I can think of lots of scenarios,
actually, but conceptually it doesn't feel like the safe default as after
updating the query the naive expectation is that the contents have been
updated.

In particular, ISTM that if we invent CREATE OR REPLACE MATERIALIZED
> VIEW, then the view content should be either computed afresh or left
> empty (depending on WITH NO DATA); it should never leave stale data.
> But I think Robert is correct that an ALTER command that does keep
> the old data is often going to be what's wanted.
>

 In any case, I'm not too hung up on what the syntax looks like or the
exact behaviour. But I do think if we're going to do something like this
that there should be a command which will create the view if it doesn't
exist, and update the query if it does. Otherwise it doesn't help at all
with writing idempotent schema definitions, which is my main reason for
using CREATE OR REPLACE.

^ permalink  raw  reply  [nested|flat] 29+ messages in thread


end of thread, other threads:[~2026-08-21 13:13 UTC | newest]

Thread overview: 29+ messages (download: mbox mbox.gz follow: Atom feed)
-- links below jump to the message on this page --
2024-07-02 01:22 CREATE OR REPLACE MATERIALIZED VIEW Erik Wienhold <ewie@ewie.name>
2024-07-02 10:46 ` Aleksander Alekseev <aleksander@timescale.com>
2024-10-31 03:48   ` Michael Paquier <michael@paquier.xyz>
2024-07-02 12:27 ` Daniel Gustafsson <daniel@yesql.se>
2024-07-02 13:58   ` Erik Wienhold <ewie@ewie.name>
2024-07-02 14:01     ` Daniel Gustafsson <daniel@yesql.se>
2024-07-04 20:18 ` Said Assemlal <sassemlal@neurorx.com>
2024-07-05 23:42   ` Erik Wienhold <ewie@ewie.name>
2024-07-12 14:49     ` Said Assemlal <sassemlal@neurorx.com>
2024-07-27 00:45       ` Erik Wienhold <ewie@ewie.name>
2024-09-05 20:33         ` Erik Wienhold <ewie@ewie.name>
2024-10-31 00:22           ` Erik Wienhold <ewie@ewie.name>
2025-01-12 21:33             ` Erik Wienhold <ewie@ewie.name>
2025-03-10 02:46               ` Erik Wienhold <ewie@ewie.name>
2025-04-05 20:37                 ` Tom Lane <tgl@sss.pgh.pa.us>
2025-08-05 01:17                   ` Erik Wienhold <ewie@ewie.name>
2026-02-03 00:19                     ` Paul A Jungwirth <pj@illuminatedcomputing.com>
2026-06-28 18:14                       ` Erik Wienhold <ewie@ewie.name>
2026-08-05 19:17                         ` Paul A Jungwirth <pj@illuminatedcomputing.com>
2026-08-14 16:28                           ` Paul A Jungwirth <pj@illuminatedcomputing.com>
2026-08-14 17:34                             ` Robert Haas <robertmhaas@gmail.com>
2026-08-14 18:05                               ` Tom Lane <tgl@sss.pgh.pa.us>
2026-08-14 18:27                                 ` Isaac Morland <isaac.morland@gmail.com>
2026-08-14 19:01                                   ` Tom Lane <tgl@sss.pgh.pa.us>
2026-08-21 12:30                                     ` Robert Haas <robertmhaas@gmail.com>
2026-08-21 13:13                                     ` Isaac Morland <isaac.morland@gmail.com>
2026-05-01 15:59                     ` Soumya S Murali <soumyamurali.work@gmail.com>
2026-06-28 18:32                       ` Erik Wienhold <ewie@ewie.name>
2026-07-23 07:05                         ` Rafia Sabih <rafia.pghackers@gmail.com>

This inbox is served by agora; see mirroring instructions
for how to clone and mirror all data and code used for this inbox