Hi! One more version of the patch with slightly more deterministic tests.
Mitar On Thu, Dec 27, 2018 at 11:43 PM Mitar <mmi...@gmail.com> wrote: > > Hi! > > I have made an updated version of the patch, added tests and > documentation changes. This is my view now a complete patch. Please > provide any feedback or comments you might have for me to improve the > patch. I will also add it to commitfest. > > A summary of the patch: This patch enables adding AFTER triggers (both > ROW and STATEMENT) on materialized views. They are fired when doing > REFRESH MATERIALIZED VIEW CONCURRENTLY for rows which have changed. > Triggers are not fired if you call REFRESH without CONCURRENTLY. This > is based on some discussion on the mailing list because implementing > it for without CONCURRENTLY would require us to add logic for firing > triggers where there was none before (and is just an efficient heap > swap). > > To be able to create a materialized view without data, specify > triggers, and REFRESH CONCURRENTLY so that those triggers would be > called also for initial data, I have tested and determined that there > is no reason why REFRESH CONCURRENTLY could not be run on > uninitialized materialized view. So I removed that check and things > seem to just work. Including triggers being called for initial data. I > think this makes REFRESH CONCURRENTLY have one less special case which > is in general nicer. > > I have run tests and all old tests still succeed. I have added more > tests for the new feature. > > I have run benchmark to evaluate the impact of me changing > refresh_by_match_merge to do UPDATE instead of DELETE and INSERT for > rows which were just updated. In fact it seems this improves > performance slightly (4% in my benchmark, mean over 10 runs). I guess > that this is because it is cheaper to just change one column's values > (what benchmark is doing when changing rows) instead of removing and > inserting the whole row. Because REFRESH MATERIALIZED VIEW > CONCURRENTLY is meant to be used when not a lot of data has been > changed anyway, I find this a pleasantly surprising additional > improvement in this patch. I am attaching the benchmark script I have > used. I compared the time of the final refresh query in the script. (I > would love if pgbench could take a custom init script to run before > the timed part of the script.) > > > Mitar > > On Mon, Dec 24, 2018 at 12:59 PM Mitar <mmi...@gmail.com> wrote: > > > > Hi! > > > > Based on discussion about observing changes on an open query in a > > reactive manner (to support reactive web applications) [1], I > > identified that one critical feature is missing to fully implement > > discussed design of having reactive queries be represented as > > materialized views, and changes to these materialized views would then > > be observed and pushed to the client through LISTEN/NOTIFY. > > > > This is my first time contributing to PostgreSQL, so I hope I am > > starting this process well. > > > > I would like to propose that support for AFTER triggers are added to > > materialized views. I experimented a bit and it seems this is mostly > > just a question of enabling/exposing them. See attached patch. This > > enabled me to add trigger to a material view which mostly worked. Here > > are my findings. > > > > Running REFRESH MATERIALIZED VIEW CONCURRENTLY calls triggers. Both > > per statement and per row. There are few improvements which could be > > done: > > > > - Currently only insert and remove operations are done on the > > materialized view. This is because the current logic just removes > > changed rows and inserts new rows. > > - In current concurrently refresh logic those insert and remove > > operations are made even if there are no changes to be done. Which > > triggers a statement trigger unnecessary. A small improvement could be > > to skip the statement in that case, but looking at the code this seems > > maybe tricky because both each of inserts and deletions are done > > inside one query each. > > - Current concurrently refresh logic does never do updates on existing > > rows. It would be nicer to have that so that triggers are more aligned > > with real changes to the data. So current two queries could be changed > > to three, each doing one of the insert, update, and delete. > > > > Non-concurrent refresh does not trigger any trigger. But it seems all > > data to do so is there (previous table, new table), at least for the > > statement-level trigger. Row-level triggers could also be simulated > > probably (with TRUNCATE and INSERT triggers). > > > > [1] > > https://www.postgresql.org/message-id/flat/CAKLmikP%2BPPB49z8rEEvRjFOD0D2DV72KdqYN7s9fjh9sM_32ZA%40mail.gmail.com > > > > > > Mitar > > > > -- > > http://mitar.tnode.com/ > > https://twitter.com/mitar_m > > > > -- > http://mitar.tnode.com/ > https://twitter.com/mitar_m -- http://mitar.tnode.com/ https://twitter.com/mitar_m
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 <replaceable class="parameter">name</replaceable> <para> <command>CREATE TRIGGER</command> 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 <replaceable class="parameter">function_name</replaceable> when certain operations are performed on that table. </para> @@ -92,6 +92,12 @@ CREATE [ CONSTRAINT ] TRIGGER <replaceable class="parameter">name</replaceable> must be marked as <literal>FOR EACH STATEMENT</literal>. </para> + <para> + <literal>AFTER</literal> triggers can be specified on materialized views and + are fired after every <command>REFRESH MATERIALIZED VIEW CONCURRENTLY</command> + on the materialized view. + </para> + <para> In addition, triggers may be defined to fire for <command>TRUNCATE</command>, though only @@ -100,7 +106,7 @@ CREATE [ CONSTRAINT ] TRIGGER <replaceable class="parameter">name</replaceable> <para> The following table summarizes which types of triggers may be used on - tables, views, and foreign tables: + tables, views, materialized views, and foreign tables: </para> <informaltable id="supported-trigger-types"> @@ -128,8 +134,8 @@ CREATE [ CONSTRAINT ] TRIGGER <replaceable class="parameter">name</replaceable> <row> <entry align="center" morerows="1"><literal>AFTER</literal></entry> <entry align="center"><command>INSERT</command>/<command>UPDATE</command>/<command>DELETE</command></entry> - <entry align="center">Tables and foreign tables</entry> - <entry align="center">Tables, views, and foreign tables</entry> + <entry align="center">Tables, materialized views, and foreign tables</entry> + <entry align="center">Tables, views, materialized views, and foreign tables</entry> </row> <row> <entry align="center"><command>TRUNCATE</command></entry> @@ -276,8 +282,8 @@ UPDATE OF <replaceable>column_name1</replaceable> [, <replaceable>column_name2</ <term><replaceable class="parameter">table_name</replaceable></term> <listitem> <para> - 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. </para> </listitem> </varlistentry> @@ -520,6 +526,17 @@ UPDATE OF <replaceable>column_name1</replaceable> [, <replaceable>column_name2</ the ones that are fired. </para> + <para> + Triggers on materialized views are fired only when + <command>REFRESH MATERIALIZED VIEW</command> is used with the + <literal>CONCURRENTLY</literal> option. Such refresh computes + a diff between old and new data and executes <command>INSERT</command>, + <command>UPDATE</command>, and <command>DELETE</command> queries to + update the materialized view to new data. + Refresh without this option refreshes all data at once and does not + fire any triggers. + </para> + <para> 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. </para> + <para> + The ability to define triggers for materialized views is a PostgreSQL + extension of the SQL standard. + </para> + </refsect1> <refsect1> 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 ] <replaceable class="parameter">name</ column names and includes all rows; that is, it must not index on any expressions nor include a <literal>WHERE</literal> clause. </para> - <para> - This option may not be used when the materialized view is not already - populated. - </para> <para> Even with this option only one <literal>REFRESH</literal> at a time may run against any one materialized view. @@ -100,6 +96,14 @@ REFRESH MATERIALIZED VIEW [ CONCURRENTLY ] <replaceable class="parameter">name</ to be ordered upon generation, you must use an <literal>ORDER BY</literal> clause in the backing query. </para> + + <para> + Refresh with the <literal>CONCURRENTLY</literal> option fires + any <literal>INSERT</literal>, <literal>UPDATE</literal>, and + <literal>DELETE</literal> triggers defined on the materialized view. + Refresh without this option refreshes all data at once and does not + fire any triggers. + </para> </refsect1> <refsect1> 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..944d608251 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 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 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 ORDER BY id 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..d150ce3824 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 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 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 ORDER BY id 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()