diff --git a/doc/src/sgml/ref/create_trigger.sgml b/doc/src/sgml/ref/create_trigger.sgml
index 6514ffc6ae..936476a558 100644
--- a/doc/src/sgml/ref/create_trigger.sgml
+++ b/doc/src/sgml/ref/create_trigger.sgml
@@ -49,8 +49,8 @@ CREATE [ CONSTRAINT ] TRIGGER name
CREATE TRIGGER creates a new trigger. The
- trigger will be associated with the specified table, view, or foreign table
- and will execute the specified
+ trigger will be associated with the specified table, view, materialized view,
+ or foreign table and will execute the specified
function function_name when
certain operations are performed on that table.
@@ -92,6 +92,12 @@ CREATE [ CONSTRAINT ] TRIGGER name
must be marked as FOR EACH STATEMENT.
+
+ AFTER triggers can be specified on materialized views and
+ are fired after every REFRESH MATERIALIZED VIEW CONCURRENTLY
+ on the materialized view.
+
+
In addition, triggers may be defined to fire for
TRUNCATE, though only
@@ -100,7 +106,7 @@ CREATE [ CONSTRAINT ] TRIGGER name
The following table summarizes which types of triggers may be used on
- tables, views, and foreign tables:
+ tables, views, materialized views, and foreign tables:
@@ -128,8 +134,8 @@ CREATE [ CONSTRAINT ] TRIGGER name
AFTER
INSERT/UPDATE/DELETE
- Tables and foreign tables
- Tables, views, and foreign tables
+ Tables, materialized views, and foreign tables
+ Tables, views, materialized views, and foreign tables
TRUNCATE
@@ -276,8 +282,8 @@ UPDATE OF column_name1 [, column_name2
table_name
- The name (optionally schema-qualified) of the table, view, or foreign
- table the trigger is for.
+ The name (optionally schema-qualified) of the table, view, materialized view,
+ or foreign table the trigger is for.
@@ -520,6 +526,17 @@ UPDATE OF column_name1 [, column_name2
the ones that are fired.
+
+ Triggers on materialized views are fired only when
+ REFRESH MATERIALIZED VIEW is used with the
+ CONCURRENTLY option. Such refresh computes
+ a diff between old and new data and executes INSERT,
+ UPDATE, and DELETE queries to
+ update the materialized view to new data.
+ Refresh without this option refreshes all data at once and does not
+ fire any triggers.
+
+
Creating a row-level trigger on a partitioned table will cause identical
triggers to be created in all its existing partitions; and any partitions
@@ -730,6 +747,11 @@ CREATE TRIGGER paired_items_update
standard.
+
+ The ability to define triggers for materialized views is a PostgreSQL
+ extension of the SQL standard.
+
+
diff --git a/doc/src/sgml/ref/refresh_materialized_view.sgml b/doc/src/sgml/ref/refresh_materialized_view.sgml
index fd06f1fda1..b0c85cb3b9 100644
--- a/doc/src/sgml/ref/refresh_materialized_view.sgml
+++ b/doc/src/sgml/ref/refresh_materialized_view.sgml
@@ -66,10 +66,6 @@ REFRESH MATERIALIZED VIEW [ CONCURRENTLY ] name
column names and includes all rows; that is, it must not index on any
expressions nor include a WHERE clause.
-
- This option may not be used when the materialized view is not already
- populated.
-
Even with this option only one REFRESH at a time may
run against any one materialized view.
@@ -100,6 +96,14 @@ REFRESH MATERIALIZED VIEW [ CONCURRENTLY ] name
to be ordered upon generation, you must use an ORDER BY
clause in the backing query.
+
+
+ Refresh with the CONCURRENTLY option fires
+ any INSERT, UPDATE, and
+ DELETE triggers defined on the materialized view.
+ Refresh without this option refreshes all data at once and does not
+ fire any triggers.
+
diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c
index a171ebabf8..feaff32cc0 100644
--- a/src/backend/commands/matview.c
+++ b/src/backend/commands/matview.c
@@ -172,12 +172,6 @@ ExecRefreshMatView(RefreshMatViewStmt *stmt, const char *queryString,
errmsg("\"%s\" is not a materialized view",
RelationGetRelationName(matviewRel))));
- /* Check that CONCURRENTLY is not specified if not populated. */
- if (concurrent && !RelationIsPopulated(matviewRel))
- ereport(ERROR,
- (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
- errmsg("CONCURRENTLY cannot be used when the materialized view is not populated")));
-
/* Check that conflicting options have not been specified. */
if (concurrent && stmt->skipData)
ereport(ERROR,
@@ -565,9 +559,11 @@ make_temptable_name_n(char *tempname, int n)
* the old record (if matched) and the ROW from the new table as a single
* column of complex record type (if matched).
*
- * Once we have the diff table, we perform set-based DELETE and INSERT
+ * Once we have the diff table, we perform set-based DELETE, UPDATE, and INSERT
* operations against the materialized view, and discard both temporary
- * tables.
+ * tables. We do all of those operations so that any triggers called because
+ * of these operations represent reasonable calls one would expect to see when
+ * syncing the materialized view to new data.
*
* Everything from the generation of the new data to applying the differences
* takes place under cover of an ExclusiveLock, since it seems as though we
@@ -590,6 +586,7 @@ refresh_by_match_merge(Oid matviewOid, Oid tempOid, Oid relowner,
bool foundUniqueIndex;
List *indexoidlist;
ListCell *indexoidscan;
+ AttrNumber relattno;
int16 relnatts;
Oid *opUsedForQual;
@@ -779,8 +776,9 @@ refresh_by_match_merge(Oid matviewOid, Oid tempOid, Oid relowner,
Assert(foundUniqueIndex);
appendStringInfoString(&querybuf,
- " AND newdata OPERATOR(pg_catalog.*=) mv) "
+ ") "
"WHERE newdata IS NULL OR mv IS NULL "
+ "OR newdata OPERATOR(pg_catalog.*<>) mv "
"ORDER BY tid");
/* Create the temporary "diff" table. */
@@ -803,7 +801,7 @@ refresh_by_match_merge(Oid matviewOid, Oid tempOid, Oid relowner,
OpenMatViewIncrementalMaintenance();
- /* Deletes must come before inserts; do them first. */
+ /* We do deletes first. */
resetStringInfo(&querybuf);
appendStringInfo(&querybuf,
"DELETE FROM %s mv WHERE ctid OPERATOR(pg_catalog.=) ANY "
@@ -814,7 +812,38 @@ refresh_by_match_merge(Oid matviewOid, Oid tempOid, Oid relowner,
if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE)
elog(ERROR, "SPI_exec failed: %s", querybuf.data);
- /* Inserts go last. */
+ /* Then we do updates. */
+ resetStringInfo(&querybuf);
+ appendStringInfo(&querybuf, "UPDATE %s mv SET (", matviewname);
+
+ for (relattno = 1; relattno <= relnatts; relattno++)
+ {
+ Form_pg_attribute attribute = TupleDescAttr(tupdesc, relattno - 1);
+ char *attributeName = NameStr(attribute->attname);
+
+ /* Ignore dropped */
+ if (attribute->attisdropped)
+ continue;
+
+ if (relattno == 1)
+ {
+ appendStringInfo(&querybuf, "%s", quote_identifier(attributeName));
+ }
+ else
+ {
+ appendStringInfo(&querybuf, ", %s", quote_identifier(attributeName));
+ }
+ }
+
+ appendStringInfo(&querybuf,
+ ") = ROW((diff.newdata).*) FROM %s diff "
+ "WHERE diff.tid IS NOT NULL AND diff.newdata IS NOT NULL "
+ "AND mv.ctid OPERATOR(pg_catalog.=) diff.tid",
+ diffname);
+ if (SPI_exec(querybuf.data, 0) != SPI_OK_UPDATE)
+ elog(ERROR, "SPI_exec failed: %s", querybuf.data);
+
+ /* Inserts and updates go last. */
resetStringInfo(&querybuf);
appendStringInfo(&querybuf,
"INSERT INTO %s SELECT (diff.newdata).* "
diff --git a/src/backend/commands/trigger.c b/src/backend/commands/trigger.c
index fb0de60a45..8597def50a 100644
--- a/src/backend/commands/trigger.c
+++ b/src/backend/commands/trigger.c
@@ -208,6 +208,16 @@ CreateTrigger(CreateTrigStmt *stmt, const char *queryString,
RelationGetRelationName(rel)),
errdetail("Tables cannot have INSTEAD OF triggers.")));
}
+ else if (rel->rd_rel->relkind == RELKIND_MATVIEW)
+ {
+ /* Materialized views can have only AFTER triggers */
+ if (stmt->timing != TRIGGER_TYPE_AFTER)
+ ereport(ERROR,
+ (errcode(ERRCODE_WRONG_OBJECT_TYPE),
+ errmsg("\"%s\" is a materialized view",
+ RelationGetRelationName(rel)),
+ errdetail("Materialized views can have only AFTER triggers.")));
+ }
else if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
{
/* Partitioned tables can't have INSTEAD OF triggers */
@@ -307,7 +317,7 @@ CreateTrigger(CreateTrigStmt *stmt, const char *queryString,
else
ereport(ERROR,
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
- errmsg("\"%s\" is not a table or view",
+ errmsg("\"%s\" is not a table, view, or materialized view",
RelationGetRelationName(rel))));
if (!allowSystemTableMods && IsSystemRelation(rel))
@@ -1513,11 +1523,12 @@ RemoveTriggerById(Oid trigOid)
if (rel->rd_rel->relkind != RELKIND_RELATION &&
rel->rd_rel->relkind != RELKIND_VIEW &&
+ rel->rd_rel->relkind != RELKIND_MATVIEW &&
rel->rd_rel->relkind != RELKIND_FOREIGN_TABLE &&
rel->rd_rel->relkind != RELKIND_PARTITIONED_TABLE)
ereport(ERROR,
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
- errmsg("\"%s\" is not a table, view, or foreign table",
+ errmsg("\"%s\" is not a table, view, materialized view, or foreign table",
RelationGetRelationName(rel))));
if (!allowSystemTableMods && IsSystemRelation(rel))
@@ -1619,11 +1630,12 @@ RangeVarCallbackForRenameTrigger(const RangeVar *rv, Oid relid, Oid oldrelid,
/* only tables and views can have triggers */
if (form->relkind != RELKIND_RELATION && form->relkind != RELKIND_VIEW &&
+ form->relkind != RELKIND_MATVIEW &&
form->relkind != RELKIND_FOREIGN_TABLE &&
form->relkind != RELKIND_PARTITIONED_TABLE)
ereport(ERROR,
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
- errmsg("\"%s\" is not a table, view, or foreign table",
+ errmsg("\"%s\" is not a table, view, materialized view, or foreign table",
rv->relname)));
/* you must own the table to rename one of its triggers */
diff --git a/src/test/regress/expected/matview.out b/src/test/regress/expected/matview.out
index 08cd4bea48..17f2029169 100644
--- a/src/test/regress/expected/matview.out
+++ b/src/test/regress/expected/matview.out
@@ -564,6 +564,104 @@ REFRESH MATERIALIZED VIEW mvtest_mv_foo;
REFRESH MATERIALIZED VIEW CONCURRENTLY mvtest_mv_foo;
DROP OWNED BY regress_user_mvtest CASCADE;
DROP ROLE regress_user_mvtest;
+-- create a new test table
+CREATE TABLE mvtest_t2 (id int NOT NULL PRIMARY KEY, type text NOT NULL, amt numeric NOT NULL);
+INSERT INTO mvtest_t2 VALUES
+ (1, 'x', 2),
+ (2, 'x', 3),
+ (3, 'y', 5),
+ (4, 'y', 7),
+ (5, 'z', 11);
+-- define trigger functions
+CREATE OR REPLACE FUNCTION notify_changes() RETURNS TRIGGER LANGUAGE plpgsql AS $$
+DECLARE
+message TEXT := TG_ARGV[0];
+BEGIN
+ IF (TG_OP = 'DELETE') THEN
+ RAISE NOTICE 'DELETE %', concat_ws(' ', message, (SELECT array_to_json(array_agg(old_table)) FROM old_table));
+ ELSIF (TG_OP = 'TRUNCATE') THEN
+ RAISE NOTICE 'TRUNCATE %', message;
+ ELSIF (TG_OP = 'UPDATE') THEN
+ RAISE NOTICE 'UPDATE %', concat_ws(' ', message, (SELECT array_to_json(array_agg(new_table)) FROM new_table), (SELECT array_to_json(array_agg(old_table)) FROM old_table));
+ ELSIF (TG_OP = 'INSERT') THEN
+ RAISE NOTICE 'INSERT %', concat_ws(' ', message, (SELECT array_to_json(array_agg(new_table)) FROM new_table));
+ END IF;
+RETURN NULL;
+END
+$$;
+CREATE OR REPLACE FUNCTION notify_changes_row() RETURNS TRIGGER LANGUAGE plpgsql AS $$
+DECLARE
+message TEXT := TG_ARGV[0];
+BEGIN
+ IF (TG_OP = 'DELETE') THEN
+ RAISE NOTICE 'DELETE %', concat_ws(' ', message, row_to_json(OLD));
+ ELSIF (TG_OP = 'UPDATE') THEN
+ RAISE NOTICE 'UPDATE %', concat_ws(' ', message, row_to_json(NEW), row_to_json(OLD));
+ ELSIF (TG_OP = 'INSERT') THEN
+ RAISE NOTICE 'INSERT %', concat_ws(' ', message, row_to_json(NEW));
+ END IF;
+RETURN NULL;
+END
+$$;
+-- create materialized view WITH NO DATA
+CREATE MATERIALIZED VIEW mvtest_t2_no_data_view AS SELECT * FROM mvtest_t2 WITH NO DATA;
+CREATE UNIQUE INDEX mvtest_t2_no_data_view_id ON mvtest_t2_no_data_view (id);
+-- register triggers
+CREATE TRIGGER mvtest_t2_no_data_view_insert AFTER INSERT ON mvtest_t2_no_data_view REFERENCING NEW TABLE AS new_table FOR EACH STATEMENT EXECUTE FUNCTION notify_changes('mvtest_t2_no_data_view_insert');
+CREATE TRIGGER mvtest_t2_no_data_view_update AFTER UPDATE ON mvtest_t2_no_data_view REFERENCING NEW TABLE AS new_table OLD TABLE AS old_table FOR EACH STATEMENT EXECUTE FUNCTION notify_changes('mvtest_t2_no_data_view_update');
+CREATE TRIGGER mvtest_t2_no_data_view_delete AFTER DELETE ON mvtest_t2_no_data_view REFERENCING OLD TABLE AS old_table FOR EACH STATEMENT EXECUTE FUNCTION notify_changes('mvtest_t2_no_data_view_delete');
+CREATE TRIGGER mvtest_t2_no_data_view_truncate AFTER TRUNCATE ON mvtest_t2_no_data_view FOR EACH STATEMENT EXECUTE FUNCTION notify_changes('mvtest_t2_no_data_view_truncate');
+CREATE TRIGGER mvtest_t2_no_data_view_insert_row AFTER INSERT ON mvtest_t2_no_data_view FOR EACH ROW EXECUTE FUNCTION notify_changes_row('mvtest_t2_no_data_view_insert_row');
+CREATE TRIGGER mvtest_t2_no_data_view_update_row AFTER UPDATE ON mvtest_t2_no_data_view FOR EACH ROW EXECUTE FUNCTION notify_changes_row('mvtest_t2_no_data_view_update_row');
+CREATE TRIGGER mvtest_t2_no_data_view_delete_row AFTER DELETE ON mvtest_t2_no_data_view FOR EACH ROW EXECUTE FUNCTION notify_changes_row('mvtest_t2_no_data_view_delete_row');
+-- try to select from view without data, it should error
+SELECT * FROM mvtest_t2_no_data_view;
+ERROR: materialized view "mvtest_t2_no_data_view" has not been populated
+HINT: Use the REFRESH MATERIALIZED VIEW command.
+-- we should be able to do initial REFRESH MATERIALIZED VIEW CONCURRENTLY and triggers should be called
+REFRESH MATERIALIZED VIEW CONCURRENTLY mvtest_t2_no_data_view;
+NOTICE: DELETE mvtest_t2_no_data_view_delete
+NOTICE: UPDATE mvtest_t2_no_data_view_update
+NOTICE: INSERT mvtest_t2_no_data_view_insert_row {"id":2,"type":"x","amt":3}
+NOTICE: INSERT mvtest_t2_no_data_view_insert_row {"id":5,"type":"z","amt":11}
+NOTICE: INSERT mvtest_t2_no_data_view_insert_row {"id":4,"type":"y","amt":7}
+NOTICE: INSERT mvtest_t2_no_data_view_insert_row {"id":1,"type":"x","amt":2}
+NOTICE: INSERT mvtest_t2_no_data_view_insert_row {"id":3,"type":"y","amt":5}
+NOTICE: INSERT mvtest_t2_no_data_view_insert [{"id":2,"type":"x","amt":3},{"id":5,"type":"z","amt":11},{"id":4,"type":"y","amt":7},{"id":1,"type":"x","amt":2},{"id":3,"type":"y","amt":5}]
+-- now materialized view should have data
+SELECT * FROM mvtest_t2_no_data_view ORDER BY id;
+ id | type | amt
+----+------+-----
+ 1 | x | 2
+ 2 | x | 3
+ 3 | y | 5
+ 4 | y | 7
+ 5 | z | 11
+(5 rows)
+
+-- update the original table
+INSERT INTO mvtest_t2 VALUES (7, 'k', 10);
+DELETE FROM mvtest_t2 WHERE id=2;
+UPDATE mvtest_t2 SET amt=5 WHERE id=4;
+-- refresh
+REFRESH MATERIALIZED VIEW CONCURRENTLY mvtest_t2_no_data_view;
+NOTICE: DELETE mvtest_t2_no_data_view_delete_row {"id":2,"type":"x","amt":3}
+NOTICE: DELETE mvtest_t2_no_data_view_delete [{"id":2,"type":"x","amt":3}]
+NOTICE: UPDATE mvtest_t2_no_data_view_update_row {"id":4,"type":"y","amt":5} {"id":4,"type":"y","amt":7}
+NOTICE: UPDATE mvtest_t2_no_data_view_update [{"id":4,"type":"y","amt":5}] [{"id":4,"type":"y","amt":7}]
+NOTICE: INSERT mvtest_t2_no_data_view_insert_row {"id":7,"type":"k","amt":10}
+NOTICE: INSERT mvtest_t2_no_data_view_insert [{"id":7,"type":"k","amt":10}]
+-- materialized view should have updated data
+SELECT * FROM mvtest_t2_no_data_view ORDER BY id;
+ id | type | amt
+----+------+-----
+ 1 | x | 2
+ 3 | y | 5
+ 4 | y | 5
+ 5 | z | 11
+ 7 | k | 10
+(5 rows)
+
-- make sure that create WITH NO DATA works via SPI
BEGIN;
CREATE FUNCTION mvtest_func()
diff --git a/src/test/regress/sql/matview.sql b/src/test/regress/sql/matview.sql
index d96175aa26..57db42f6cd 100644
--- a/src/test/regress/sql/matview.sql
+++ b/src/test/regress/sql/matview.sql
@@ -223,6 +223,80 @@ REFRESH MATERIALIZED VIEW CONCURRENTLY mvtest_mv_foo;
DROP OWNED BY regress_user_mvtest CASCADE;
DROP ROLE regress_user_mvtest;
+-- create a new test table
+CREATE TABLE mvtest_t2 (id int NOT NULL PRIMARY KEY, type text NOT NULL, amt numeric NOT NULL);
+INSERT INTO mvtest_t2 VALUES
+ (1, 'x', 2),
+ (2, 'x', 3),
+ (3, 'y', 5),
+ (4, 'y', 7),
+ (5, 'z', 11);
+
+-- define trigger functions
+CREATE OR REPLACE FUNCTION notify_changes() RETURNS TRIGGER LANGUAGE plpgsql AS $$
+DECLARE
+message TEXT := TG_ARGV[0];
+BEGIN
+ IF (TG_OP = 'DELETE') THEN
+ RAISE NOTICE 'DELETE %', concat_ws(' ', message, (SELECT array_to_json(array_agg(old_table)) FROM old_table));
+ ELSIF (TG_OP = 'TRUNCATE') THEN
+ RAISE NOTICE 'TRUNCATE %', message;
+ ELSIF (TG_OP = 'UPDATE') THEN
+ RAISE NOTICE 'UPDATE %', concat_ws(' ', message, (SELECT array_to_json(array_agg(new_table)) FROM new_table), (SELECT array_to_json(array_agg(old_table)) FROM old_table));
+ ELSIF (TG_OP = 'INSERT') THEN
+ RAISE NOTICE 'INSERT %', concat_ws(' ', message, (SELECT array_to_json(array_agg(new_table)) FROM new_table));
+ END IF;
+RETURN NULL;
+END
+$$;
+CREATE OR REPLACE FUNCTION notify_changes_row() RETURNS TRIGGER LANGUAGE plpgsql AS $$
+DECLARE
+message TEXT := TG_ARGV[0];
+BEGIN
+ IF (TG_OP = 'DELETE') THEN
+ RAISE NOTICE 'DELETE %', concat_ws(' ', message, row_to_json(OLD));
+ ELSIF (TG_OP = 'UPDATE') THEN
+ RAISE NOTICE 'UPDATE %', concat_ws(' ', message, row_to_json(NEW), row_to_json(OLD));
+ ELSIF (TG_OP = 'INSERT') THEN
+ RAISE NOTICE 'INSERT %', concat_ws(' ', message, row_to_json(NEW));
+ END IF;
+RETURN NULL;
+END
+$$;
+
+-- create materialized view WITH NO DATA
+CREATE MATERIALIZED VIEW mvtest_t2_no_data_view AS SELECT * FROM mvtest_t2 WITH NO DATA;
+CREATE UNIQUE INDEX mvtest_t2_no_data_view_id ON mvtest_t2_no_data_view (id);
+
+-- register triggers
+CREATE TRIGGER mvtest_t2_no_data_view_insert AFTER INSERT ON mvtest_t2_no_data_view REFERENCING NEW TABLE AS new_table FOR EACH STATEMENT EXECUTE FUNCTION notify_changes('mvtest_t2_no_data_view_insert');
+CREATE TRIGGER mvtest_t2_no_data_view_update AFTER UPDATE ON mvtest_t2_no_data_view REFERENCING NEW TABLE AS new_table OLD TABLE AS old_table FOR EACH STATEMENT EXECUTE FUNCTION notify_changes('mvtest_t2_no_data_view_update');
+CREATE TRIGGER mvtest_t2_no_data_view_delete AFTER DELETE ON mvtest_t2_no_data_view REFERENCING OLD TABLE AS old_table FOR EACH STATEMENT EXECUTE FUNCTION notify_changes('mvtest_t2_no_data_view_delete');
+CREATE TRIGGER mvtest_t2_no_data_view_truncate AFTER TRUNCATE ON mvtest_t2_no_data_view FOR EACH STATEMENT EXECUTE FUNCTION notify_changes('mvtest_t2_no_data_view_truncate');
+CREATE TRIGGER mvtest_t2_no_data_view_insert_row AFTER INSERT ON mvtest_t2_no_data_view FOR EACH ROW EXECUTE FUNCTION notify_changes_row('mvtest_t2_no_data_view_insert_row');
+CREATE TRIGGER mvtest_t2_no_data_view_update_row AFTER UPDATE ON mvtest_t2_no_data_view FOR EACH ROW EXECUTE FUNCTION notify_changes_row('mvtest_t2_no_data_view_update_row');
+CREATE TRIGGER mvtest_t2_no_data_view_delete_row AFTER DELETE ON mvtest_t2_no_data_view FOR EACH ROW EXECUTE FUNCTION notify_changes_row('mvtest_t2_no_data_view_delete_row');
+
+-- try to select from view without data, it should error
+SELECT * FROM mvtest_t2_no_data_view;
+
+-- we should be able to do initial REFRESH MATERIALIZED VIEW CONCURRENTLY and triggers should be called
+REFRESH MATERIALIZED VIEW CONCURRENTLY mvtest_t2_no_data_view;
+
+-- now materialized view should have data
+SELECT * FROM mvtest_t2_no_data_view ORDER BY id;
+
+-- update the original table
+INSERT INTO mvtest_t2 VALUES (7, 'k', 10);
+DELETE FROM mvtest_t2 WHERE id=2;
+UPDATE mvtest_t2 SET amt=5 WHERE id=4;
+
+-- refresh
+REFRESH MATERIALIZED VIEW CONCURRENTLY mvtest_t2_no_data_view;
+
+-- materialized view should have updated data
+SELECT * FROM mvtest_t2_no_data_view ORDER BY id;
+
-- make sure that create WITH NO DATA works via SPI
BEGIN;
CREATE FUNCTION mvtest_func()