Hi hackers, Thank you for your many questions and feedbacks at PGCon 2019. Attached is the patch rebased for the current master branch.
Regards, Yugo Nagata On Tue, 14 May 2019 15:46:48 +0900 Yugo Nagata <nag...@sraoss.co.jp> wrote: > On Mon, 1 Apr 2019 12:11:22 +0900 > Yugo Nagata <nag...@sraoss.co.jp> wrote: > > > On Thu, 27 Dec 2018 21:57:26 +0900 > > Yugo Nagata <nag...@sraoss.co.jp> wrote: > > > > > Hi, > > > > > > I would like to implement Incremental View Maintenance (IVM) on > > > PostgreSQL. > > > > I am now working on an initial patch for implementing IVM on PostgreSQL. > > This enables materialized views to be updated incrementally after one > > of their base tables is modified. > > Attached is a WIP patch of Incremental View Maintenance (IVM). > Major part is written by me, and changes in syntax and pg_class > are Hoshiai-san's work. > > Although this is sill a draft patch in work-in-progress, any > suggestions or thoughts would be appreciated. > > * What it is > > This allows a kind of Immediate Maintenance of materialized views. if a > materialized view is created by CRATE INCREMENTAL MATERIALIZED VIEW command, > the contents of the mateview is updated automatically and incrementally > after base tables are updated. Noted this syntax is just tentative, so it > may be changed. > > ====== Example 1 ====== > postgres=# CREATE INCREMENTAL MATERIALIZED VIEW m AS SELECT * FROM t0; > SELECT 3 > postgres=# SELECT * FROM m; > i > --- > 3 > 2 > 1 > (3 rows) > > postgres=# INSERT INTO t0 VALUES (4); > INSERT 0 1 > postgres=# SELECt * FROM m; -- automatically updated > i > --- > 3 > 2 > 1 > 4 > (4 rows) > ============================= > > This implementation also supports matviews including duplicate tuples or > DISTINCT clause in its view definition query. For example, even if a matview > is defined with DISTINCT to remove duplication of tuples in a base table, this > can perform incremental update of the matview properly. That is, the contents > of the matview doesn't change when exiting tuples are inserted into the base > tables, and a tuple in the matview is deleted only when duplicity of the > corresponding tuple in the base table becomes zero. > > This is due to "colunting alogorithm" in which the number of each tuple is > stored in matviews as a special column value. > > ====== Example 2 ====== > postgres=# SELECT * FROM t1; > id | t > ----+--- > 1 | A > 2 | B > 3 | C > 4 | A > (4 rows) > > postgres=# CREATE INCREMENTAL MATERIALIZED VIEW m1 AS SELECT t FROM t1; > SELECT 3 > postgres=# CREATE INCREMENTAL MATERIALIZED VIEW m2 AS SELECT DISTINCT t FROM > t1; > SELECT 3 > postgres=# SELECT * FROM m1; -- with duplicity > t > --- > A > A > C > B > (4 rows) > > postgres=# SELECT * FROM m2; > t > --- > A > B > C > (3 rows) > > postgres=# INSERT INTO t1 VALUES (5, 'B'); > INSERT 0 1 > postgres=# DELETE FROM t1 WHERE id IN (1,3); -- delete (1,A),(3,C) > DELETE 2 > postgres=# SELECT * FROM m1; -- one A left and one more B > t > --- > B > B > A > (3 rows) > > postgres=# SELECT * FROM m2; -- only C is removed > t > --- > B > A > (2 rows) > ============================= > > * How it works > > 1. Creating matview > > When a matview is created, AFTER triggers are internally created > on its base tables. When the base tables is modified (INSERT, DELETE, > UPDATE), the matview is updated incrementally in the trigger function. > > When populating the matview, GROUP BY and count(*) are added to the > view definition query before this is executed for counting duplicity > of tuples in the matview. The result of count is stored in the matview > as a special column named "__ivm_count__". > > 2. Maintenance of matview > > When base tables are modified, the change set of the table can be > referred as Ephemeral Named Relations (ENRs) thanks to Transition Table > (a feature of trigger implemented since PG10). We can calculate the diff > set of the matview by replacing the base table in the view definition > query with the ENR (at least if it is Selection-Projection -Join view). > As well as view definition time, GROUP BY and count(*) is added in order > to count the duplicity of tuples in the diff set. As a result, two diff > sets (to be deleted from and to be inserted into the matview) are > calculated, and the results are stored into temporary tables respectively. > > The matiview is updated by merging these change sets. Instead of executing > DELETE or INSERT simply, the values of __ivm_count__ column in the matview > is decreased or increased. When the values becomes zero, the corresponding > tuple is deleted from the matview. > > 3. Access to matview > > When SELECT is issued for IVM matviews defined with DISTINCT, all columns > except to __ivm_count__ of each tuple in the matview are returned. This is > correct because duplicity of tuples are eliminated by GROUP BY. > > When DISTINCT is not used, SELECT for the IVM matviews returns each tuple > __ivm_count__ times. Currently, this is implemented by rewriting the SELECT > query to replace the matview RTE with a subquery which joins the matview > and generate_series function as bellow. > > SELECT mv.* FROM mv, generate_series(1, mv.__ivm_count__); > > __ivm_count__ column is invisible for users when "SELECT * FROM ..." is > issued, but users can see the value by specifying in target list explicitly. > > ====== Example 3 ====== > postgres=# \d+ m1 > Materialized view "public.m1" > Column | Type | Collation | Nullable | Default | Storage | Stats > target | Description > ---------------+--------+-----------+----------+---------+----------+--------------+------------- > t | text | | | | extended | > | > __ivm_count__ | bigint | | | | plain | > | > View definition: > SELECT t1.t > FROM t1; > Access method: heap > > postgres=# \d+ m2 > Materialized view "public.m2" > Column | Type | Collation | Nullable | Default | Storage | Stats > target | Description > ---------------+--------+-----------+----------+---------+----------+--------------+------------- > t | text | | | | extended | > | > __ivm_count__ | bigint | | | | plain | > | > View definition: > SELECT DISTINCT t1.t > FROM t1; > Access method: heap > > postgres=# SELECT *, __ivm_count__ FROM m1; > t | __ivm_count__ > ---+--------------- > B | 2 > B | 2 > A | 1 > (3 rows) > > postgres=# SELECT *, __ivm_count__ FROM m2; > t | __ivm_count__ > ---+--------------- > B | 2 > A | 1 > (2 rows) > > postgres=# EXPLAIN SELECT * FROM m1; > QUERY PLAN > ------------------------------------------------------------------------------ > Nested Loop (cost=0.00..61.03 rows=3000 width=2) > -> Seq Scan on m1 mv (cost=0.00..1.03 rows=3 width=10) > -> Function Scan on generate_series (cost=0.00..10.00 rows=1000 width=0) > (3 rows) > ============================= > > * Simple Performance Evaluation > > I confirmed that "incremental" update of matviews is more effective > than the standard REFRESH by using simple exapmle. I used tables > of pgbench (SF=100) here. > > Create two matviews, that is, without and with IVM. > > test=# CREATE MATERIALIZED VIEW bench1 AS > SELECT aid, bid, abalance, bbalance > FROM pgbench_accounts JOIN pgbench_branches USING (bid) > WHERE abalance > 0 OR bbalance > 0; > SELECT 5001054 > test=# CREATE INCREMENTAL MATERIALIZED VIEW bench2 AS > SELECT aid, bid, abalance, bbalance > FROM pgbench_accounts JOIN pgbench_branches USING (bid) > WHERE abalance > 0 OR bbalance > 0; > SELECT 5001054 > > The standard REFRESH of bench1 took more than 10 seconds. > > test=# \timing > Timing is on. > test=# REFRESH MATERIALIZED VIEW bench1 ; > REFRESH MATERIALIZED VIEW > Time: 11210.563 ms (00:11.211) > > Create an index on the IVM matview (bench2). > > test=# CREATE INDEX on bench2(aid,bid); > CREATE INDEX > > Updating a tuple in pgbench_accounts took 18ms. After this, bench2 > was updated automatically and correctly. > > test=# SELECT * FROM bench2 WHERE aid = 1; > aid | bid | abalance | bbalance > -----+-----+----------+---------- > 1 | 1 | 10 | 10 > (1 row) > > Time: 2.498 ms > test=# UPDATE pgbench_accounts SET abalance = 1000 WHERE aid = 1; > UPDATE 1 > Time: 18.634 ms > test=# SELECT * FROM bench2 WHERE aid = 1; > aid | bid | abalance | bbalance > -----+-----+----------+---------- > 1 | 1 | 1000 | 10 > (1 row) > > However, if there is not the index on bench2, it took 4 sec, so > appropriate indexes are needed on IVM matviews. > > test=# DROP INDEX bench2_aid_bid_idx ; > DROP INDEX > Time: 10.613 ms > test=# UPDATE pgbench_accounts SET abalance = 2000 WHERE aid = 1; > UPDATE 1 > Time: 3931.274 ms (00:03.931) > > * Restrictions on view definition > > This patch is still in Work-in-Progress and there are many restrictions > on the view definition query of matviews. > > The current implementation supports views including selection, projection, > and inner join with or without DISTINCT. Aggregation and GROUP BY are not > supported yet, but I plan to deal with these by the first release. > Self-join, subqueries, OUTER JOIN, CTE, window functions are not > considered well, either. I need more investigation on these type of views > although I found some papers explaining how to handle sub-queries and > outer-joins. > > These unsupported views should be checked when a matview is created, but > this is not implemented yet. Hoshiai-san are working on this. > > * Timing of view maintenance > > This patch implements a kind of Immediate Maintenance, that is, a matview > is updated immediately when a base table is modified. On other hand, in > "Deferred Maintenance", matviews are updated after the transaction, for > example, by the user command like REFRESH. > > For implementing "deferred", it is need to implement a mechanism to maintain > logs for recording changes of base tables and an algorithm to compute the > delta to be applied to matviews. > > In addition, there could be another implementation of Immediate Maintenance > in which matview is updated at the end of a transaction that modified base > table, rather than in AFTER trigger. Oracle supports this type of IVM. To > implement this, we will need a mechanism to maintain change logs on base > tables as well as Deferred maintenance. > > * Counting algorithm implementation > > There will be also discussions on counting-algorithm implementation. > Firstly, the current patch treats "__ivm_count__" as a special column name > in a somewhat ad hoc way. This is used when maintaining and accessing > matviews, > and when "SELECT * FROM ..." is issued, __ivm_count__ column is invisible for > users. Maybe this name has to be inhibited in user tables. Is it acceptable > to use such columns for IVM, and is there better way, if not? > > Secondly, a matview with duplicate tuples is replaces with a subquery which > uses generate_series function. It does not have to be generate_series, and we > can make a new set returning function for this. Anyway, this internal > behaviour > is visible in EXPLAIN results as shown in Example 3. Also, there is a > performance impact because estimated rows number is wrong, and what is > worse, > the cost of join is not small when the size of matview is large. Therefore, we > might have to add a new plan node for selecting from matviews rather than > using > such a special set returning function. > > > Ragards, > -- > Yugo Nagata <nag...@sraoss.co.jp> -- Yugo Nagata <nag...@sraoss.co.jp>
diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml index 1300c7bbaa..ddac6032ba 100644 --- a/doc/src/sgml/catalogs.sgml +++ b/doc/src/sgml/catalogs.sgml @@ -1949,6 +1949,13 @@ SCRAM-SHA-256$<replaceable><iteration count></replaceable>:<replaceable>&l <entry>True if table or index is a partition</entry> </row> + <row> + <entry><structfield>relisivm</structfield></entry> + <entry><type>bool</type></entry> + <entry></entry> + <entry>True if materialized view enables incremental view maintenance</entry> + </row> + <row> <entry><structfield>relrewrite</structfield></entry> <entry><type>oid</type></entry> diff --git a/doc/src/sgml/ref/create_materialized_view.sgml b/doc/src/sgml/ref/create_materialized_view.sgml index ec8847ed40..a23366a342 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 [ INCREMENTAL ] 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>] [, ... ] ) ] @@ -54,6 +54,16 @@ CREATE MATERIALIZED VIEW [ IF NOT EXISTS ] <replaceable>table_name</replaceable> <title>Parameters</title> <variablelist> + <varlistentry> + <term><literal>INCREMENTAL</literal></term> + <listitem> + <para> + If specified, a materialized view enables incremental view maintenance. + You can replace only the contents of a materialized view, which based rows are changed. + </para> + </listitem> + </varlistentry> + <varlistentry> <term><literal>IF NOT EXISTS</literal></term> <listitem> diff --git a/src/backend/catalog/heap.c b/src/backend/catalog/heap.c index 86820eecfc..f0f0e3bb84 100644 --- a/src/backend/catalog/heap.c +++ b/src/backend/catalog/heap.c @@ -891,6 +891,7 @@ InsertPgClassTuple(Relation pg_class_desc, values[Anum_pg_class_relrewrite - 1] = ObjectIdGetDatum(rd_rel->relrewrite); values[Anum_pg_class_relfrozenxid - 1] = TransactionIdGetDatum(rd_rel->relfrozenxid); values[Anum_pg_class_relminmxid - 1] = MultiXactIdGetDatum(rd_rel->relminmxid); + values[Anum_pg_class_relisivm - 1] = BoolGetDatum(rd_rel->relisivm); if (relacl != (Datum) 0) values[Anum_pg_class_relacl - 1] = relacl; else diff --git a/src/backend/catalog/index.c b/src/backend/catalog/index.c index 587b717242..d26cdfc057 100644 --- a/src/backend/catalog/index.c +++ b/src/backend/catalog/index.c @@ -910,6 +910,7 @@ index_create(Relation heapRelation, indexRelation->rd_rel->relowner = heapRelation->rd_rel->relowner; indexRelation->rd_rel->relam = accessMethodObjectId; indexRelation->rd_rel->relispartition = OidIsValid(parentIndexRelid); + indexRelation->rd_rel->relisivm = false; /* * store index's pg_class entry diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index 4c1d909d38..34678666a3 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -51,6 +51,14 @@ #include "utils/rls.h" #include "utils/snapmgr.h" +#include "catalog/pg_trigger.h" +#include "catalog/pg_type.h" +#include "commands/trigger.h" +#include "parser/parser.h" +#include "parser/parsetree.h" +#include "parser/parse_func.h" +#include "nodes/print.h" + typedef struct { @@ -74,6 +82,8 @@ static bool intorel_receive(TupleTableSlot *slot, DestReceiver *self); static void intorel_shutdown(DestReceiver *self); static void intorel_destroy(DestReceiver *self); +static void CreateIvmTrigger(Oid relOid, Oid viewOid, char *matviewname, int16 type); +static void CreateIvmTriggersOnBaseTables(Query *qry, Node *jtnode, Oid matviewOid, char* matviewname); /* * create_ctas_internal @@ -109,6 +119,8 @@ create_ctas_internal(List *attrList, IntoClause *into) create->oncommit = into->onCommit; create->tablespacename = into->tableSpaceName; create->if_not_exists = false; + /* Using Materialized view only */ + create->ivm = into->ivm; create->accessMethod = into->accessMethod; /* @@ -239,6 +251,7 @@ ExecCreateTableAs(CreateTableAsStmt *stmt, const char *queryString, List *rewritten; PlannedStmt *plan; QueryDesc *queryDesc; + Query *copied_query; if (stmt->if_not_exists) { @@ -319,7 +332,29 @@ ExecCreateTableAs(CreateTableAsStmt *stmt, const char *queryString, * and is executed repeatedly. (See also the same hack in EXPLAIN and * PREPARE.) */ - rewritten = QueryRewrite(copyObject(query)); + + copied_query = copyObject(query); + if (is_matview && into->ivm) + { + TargetEntry *tle; + Node *node; + ParseState *pstate = make_parsestate(NULL); + + FuncCall *fn = makeFuncCall(list_make1(makeString("count")), NIL, -1); + fn->agg_star = true; + + copied_query->groupClause = transformDistinctClause(NULL, &copied_query->targetList, copied_query->sortClause, false); + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(copied_query->targetList) + 1, + pstrdup("__ivm_count__"), + false); + copied_query->targetList = lappend(copied_query->targetList, tle); + copied_query->hasAggs = true; + } + + rewritten = QueryRewrite(copied_query); /* SELECT should never rewrite to more or less than one SELECT query */ if (list_length(rewritten) != 1) @@ -378,11 +413,65 @@ ExecCreateTableAs(CreateTableAsStmt *stmt, const char *queryString, /* Restore userid and security context */ SetUserIdAndSecContext(save_userid, save_sec_context); + + + if (into->ivm) + { + char *matviewname; + Oid matviewOid = address.objectId; + Relation matviewRel = table_open(matviewOid, NoLock); + matviewname = quote_qualified_identifier(get_namespace_name(RelationGetNamespace(matviewRel)), + RelationGetRelationName(matviewRel)); + copied_query = copyObject(query); + AcquireRewriteLocks(copied_query, true, false); + + CreateIvmTriggersOnBaseTables(copied_query, (Node *)copied_query->jointree, matviewOid, matviewname); + + table_close(matviewRel, NoLock); + } } return address; } +static void CreateIvmTriggersOnBaseTables(Query *qry, Node *jtnode, Oid matviewOid, char* matviewname) +{ + + if (jtnode == NULL) + return; + if (IsA(jtnode, RangeTblRef)) + { + int rti = ((RangeTblRef *) jtnode)->rtindex; + RangeTblEntry *rte = rt_fetch(rti, qry->rtable); + + if (rte->rtekind == RTE_RELATION) + { + CreateIvmTrigger(rte->relid, matviewOid, matviewname, TRIGGER_TYPE_INSERT); + CreateIvmTrigger(rte->relid, matviewOid, matviewname, TRIGGER_TYPE_DELETE); + CreateIvmTrigger(rte->relid, matviewOid, matviewname, TRIGGER_TYPE_UPDATE); + } + else + elog(ERROR, "unsupported RTE kind: %d", (int) rte->rtekind); + } + else if (IsA(jtnode, FromExpr)) + { + FromExpr *f = (FromExpr *) jtnode; + ListCell *l; + + foreach(l, f->fromlist) + CreateIvmTriggersOnBaseTables(qry, lfirst(l), matviewOid, matviewname); + } + else if (IsA(jtnode, JoinExpr)) + { + JoinExpr *j = (JoinExpr *) jtnode; + + CreateIvmTriggersOnBaseTables(qry, j->larg, matviewOid, matviewname); + CreateIvmTriggersOnBaseTables(qry, j->rarg, matviewOid, matviewname); + } + else + elog(ERROR, "unrecognized node type: %d", (int) nodeTag(jtnode)); +} + /* * GetIntoRelEFlags --- compute executor flags needed for CREATE TABLE AS * @@ -547,6 +636,11 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) if (is_matview && !into->skipData) SetMatViewPopulatedState(intoRelationDesc, true); + /* + * Mark relisivm field, if it's a matview and into->ivm is true. + */ + if (is_matview && into->ivm) + SetMatViewIVMState(intoRelationDesc, true); /* * Fill private fields of myState for use by later routines */ @@ -619,3 +713,74 @@ intorel_destroy(DestReceiver *self) { pfree(self); } + +static void +CreateIvmTrigger(Oid relOid, Oid viewOid, char *matviewname, int16 type) +{ + CreateTrigStmt *ivm_trigger; + List *transitionRels = NIL; + ObjectAddress address, refaddr; + + refaddr.classId = RelationRelationId; + refaddr.objectId = viewOid; + refaddr.objectSubId = 0; + + + ivm_trigger = makeNode(CreateTrigStmt); + ivm_trigger->relation = NULL; + ivm_trigger->row = false; + ivm_trigger->timing = TRIGGER_TYPE_AFTER; + + ivm_trigger->events = type; + + switch (type) + { + case TRIGGER_TYPE_INSERT: + ivm_trigger->trigname = "IVM_trigger_ins"; + break; + case TRIGGER_TYPE_DELETE: + ivm_trigger->trigname = "IVM_trigger_del"; + break; + case TRIGGER_TYPE_UPDATE: + ivm_trigger->trigname = "IVM_trigger_upd"; + break; + } + + if (type == TRIGGER_TYPE_INSERT || type == TRIGGER_TYPE_UPDATE) + { + TriggerTransition *n = makeNode(TriggerTransition); + n->name = "ivm_newtable"; + n->isNew = true; + n->isTable = true; + + transitionRels = lappend(transitionRels, n); + } + if (type == TRIGGER_TYPE_DELETE || type == TRIGGER_TYPE_UPDATE) + { + TriggerTransition *n = makeNode(TriggerTransition); + n->name = "ivm_oldtable"; + n->isNew = false; + n->isTable = true; + + transitionRels = lappend(transitionRels, n); + } + + ivm_trigger->funcname = SystemFuncName("IVM_immediate_maintenance"); + + ivm_trigger->columns = NIL; + ivm_trigger->transitionRels = transitionRels; + ivm_trigger->whenClause = NULL; + ivm_trigger->isconstraint = false; + ivm_trigger->deferrable = false; + ivm_trigger->initdeferred = false; + ivm_trigger->constrrel = NULL; + ivm_trigger->args = list_make1(makeString(matviewname)); + + address = CreateTrigger(ivm_trigger, NULL, relOid, InvalidOid, InvalidOid, + InvalidOid, InvalidOid, InvalidOid, NULL, true, false); + + recordDependencyOn(&address, &refaddr, DEPENDENCY_AUTO); + + /* Make changes-so-far visible */ + CommandCounterIncrement(); +} diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index 537d0e8cef..05ab22715d 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -46,6 +46,11 @@ #include "utils/snapmgr.h" #include "utils/syscache.h" +#include "utils/regproc.h" +#include "nodes/makefuncs.h" +#include "parser/parse_clause.h" +#include "parser/parse_func.h" + typedef struct { @@ -65,7 +70,8 @@ static bool transientrel_receive(TupleTableSlot *slot, DestReceiver *self); static void transientrel_shutdown(DestReceiver *self); static void transientrel_destroy(DestReceiver *self); static uint64 refresh_matview_datafill(DestReceiver *dest, Query *query, - const char *queryString); + QueryEnvironment *queryEnv, + const char *queryString); static char *make_temptable_name_n(char *tempname, int n); static void refresh_by_match_merge(Oid matviewOid, Oid tempOid, Oid relowner, int save_sec_context); @@ -74,6 +80,9 @@ static bool is_usable_unique_index(Relation indexRel); static void OpenMatViewIncrementalMaintenance(void); static void CloseMatViewIncrementalMaintenance(void); +static void apply_delta(Oid matviewOid, Oid tempOid_new, Oid tempOid_old, + Oid relowner, int save_sec_context); + /* * SetMatViewPopulatedState * Mark a materialized view as populated, or not. @@ -114,6 +123,46 @@ SetMatViewPopulatedState(Relation relation, bool newstate) CommandCounterIncrement(); } +/* + * SetMatViewIVMState + * Mark a materialized view as IVM, or not. + * + * NOTE: caller must be holding an appropriate lock on the relation. + */ +void +SetMatViewIVMState(Relation relation, bool newstate) +{ + Relation pgrel; + HeapTuple tuple; + + Assert(relation->rd_rel->relkind == RELKIND_MATVIEW); + + /* + * Update relation's pg_class entry. Crucial side-effect: other backends + * (and this one too!) are sent SI message to make them rebuild relcache + * entries. + */ + pgrel = table_open(RelationRelationId, RowExclusiveLock); + tuple = SearchSysCacheCopy1(RELOID, + ObjectIdGetDatum(RelationGetRelid(relation))); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for relation %u", + RelationGetRelid(relation)); + + ((Form_pg_class) GETSTRUCT(tuple))->relisivm = newstate; + + CatalogTupleUpdate(pgrel, &tuple->t_self, tuple); + + heap_freetuple(tuple); + table_close(pgrel, RowExclusiveLock); + + /* + * Advance command counter to make the updated pg_class row locally + * visible. + */ + CommandCounterIncrement(); +} + /* * ExecRefreshMatView -- execute a REFRESH MATERIALIZED VIEW command * @@ -311,7 +360,7 @@ ExecRefreshMatView(RefreshMatViewStmt *stmt, const char *queryString, /* Generate the data, if wanted. */ if (!stmt->skipData) - processed = refresh_matview_datafill(dest, dataQuery, queryString); + processed = refresh_matview_datafill(dest, dataQuery, NULL, queryString); /* Make the matview match the newly generated data. */ if (concurrent) @@ -369,6 +418,7 @@ ExecRefreshMatView(RefreshMatViewStmt *stmt, const char *queryString, */ static uint64 refresh_matview_datafill(DestReceiver *dest, Query *query, + QueryEnvironment *queryEnv, const char *queryString) { List *rewritten; @@ -405,7 +455,7 @@ refresh_matview_datafill(DestReceiver *dest, Query *query, /* Create a QueryDesc, redirecting output to our tuple receiver */ queryDesc = CreateQueryDesc(plan, queryString, GetActiveSnapshot(), InvalidSnapshot, - dest, NULL, NULL, 0); + dest, NULL, queryEnv ? queryEnv: NULL, 0); /* call ExecutorStart to prepare the plan for execution */ ExecutorStart(queryDesc, 0); @@ -926,3 +976,366 @@ CloseMatViewIncrementalMaintenance(void) matview_maintenance_depth--; Assert(matview_maintenance_depth >= 0); } + +/* + * IVM trigger function + */ + +Datum +IVM_immediate_maintenance(PG_FUNCTION_ARGS) +{ + TriggerData *trigdata = (TriggerData *) fcinfo->context; + Relation rel; + Oid relid; + Oid matviewOid; + Query *query, *old_delta_qry, *new_delta_qry; + char* matviewname = trigdata->tg_trigger->tgargs[0]; + List *names; + Relation matviewRel; + int old_depth = matview_maintenance_depth; + + Oid tableSpace; + Oid relowner; + Oid OIDDelta_new = InvalidOid; + Oid OIDDelta_old = InvalidOid; + DestReceiver *dest_new = NULL, *dest_old = NULL; + char relpersistence; + Oid save_userid; + int save_sec_context; + int save_nestlevel; + + ParseState *pstate; + QueryEnvironment *queryEnv = create_queryEnv(); + + /* Create a dummy ParseState for addRangeTableEntryForENR */ + pstate = make_parsestate(NULL); + pstate->p_queryEnv = queryEnv; + + names = stringToQualifiedNameList(matviewname); + matviewOid = RangeVarGetRelid(makeRangeVarFromNameList(names), AccessShareLock, true); + matviewRel = table_open(matviewOid, NoLock); + + /* Make sure it is a materialized view. */ + if (matviewRel->rd_rel->relkind != RELKIND_MATVIEW) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("\"%s\" is not a materialized view", + RelationGetRelationName(matviewRel)))); + + rel = trigdata->tg_relation; + relid = rel->rd_id; + + query = get_view_query(matviewRel); + + new_delta_qry = copyObject(query); + old_delta_qry = copyObject(query); + + if (trigdata->tg_newtable) + { + RangeTblEntry *rte; + ListCell *lc; + + TargetEntry *tle; + Node *node; + FuncCall *fn = makeFuncCall(list_make1(makeString("count")), NIL, -1); + + EphemeralNamedRelation enr = + palloc(sizeof(EphemeralNamedRelationData)); + + enr->md.name = trigdata->tg_trigger->tgnewtable; + enr->md.reliddesc = trigdata->tg_relation->rd_id; + enr->md.tupdesc = NULL; + enr->md.enrtype = ENR_NAMED_TUPLESTORE; + enr->md.enrtuples = tuplestore_tuple_count(trigdata->tg_newtable); + enr->reldata = trigdata->tg_newtable; + register_ENR(queryEnv, enr); + + rte = addRangeTableEntryForENR(pstate, makeRangeVar(NULL, enr->md.name, -1), true); + new_delta_qry->rtable = lappend(new_delta_qry->rtable, rte); + + foreach(lc, new_delta_qry->rtable) + { + RangeTblEntry *r = (RangeTblEntry*) lfirst(lc); + if (r->relid == relid) + { + lfirst(lc) = rte; + break; + } + } + + fn->agg_star = true; + new_delta_qry->groupClause = transformDistinctClause(NULL, &new_delta_qry->targetList, new_delta_qry->sortClause, false); + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(new_delta_qry->targetList) + 1, + pstrdup("__ivm_count__"), + false); + new_delta_qry->targetList = lappend(new_delta_qry->targetList, tle); + new_delta_qry->hasAggs = true; + } + + if (trigdata->tg_oldtable) + { + RangeTblEntry *rte; + ListCell *lc; + + TargetEntry *tle; + Node *node; + FuncCall *fn = makeFuncCall(list_make1(makeString("count")), NIL, -1); + + EphemeralNamedRelation enr = + palloc(sizeof(EphemeralNamedRelationData)); + + enr->md.name = trigdata->tg_trigger->tgoldtable; + enr->md.reliddesc = trigdata->tg_relation->rd_id; + enr->md.tupdesc = NULL; + enr->md.enrtype = ENR_NAMED_TUPLESTORE; + enr->md.enrtuples = tuplestore_tuple_count(trigdata->tg_oldtable); + enr->reldata = trigdata->tg_oldtable; + register_ENR(queryEnv, enr); + + rte = addRangeTableEntryForENR(pstate, makeRangeVar(NULL, enr->md.name, -1), true); + old_delta_qry->rtable = lappend(old_delta_qry->rtable, rte); + + foreach(lc, old_delta_qry->rtable) + { + RangeTblEntry *r = (RangeTblEntry*) lfirst(lc); + if (r->relid == relid) + { + lfirst(lc) = rte; + break; + } + } + + fn->agg_star = true; + old_delta_qry->groupClause = transformDistinctClause(NULL, &old_delta_qry->targetList, old_delta_qry->sortClause, false); + node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1); + + tle = makeTargetEntry((Expr *) node, + list_length(old_delta_qry->targetList) + 1, + pstrdup("__ivm_count__"), + false); + old_delta_qry->targetList = lappend(old_delta_qry->targetList, tle); + old_delta_qry->hasAggs = true; + } + + + /* + * Check for active uses of the relation in the current transaction, such + * as open scans. + * + * NB: We count on this to protect us against problems with refreshing the + * data using TABLE_INSERT_FROZEN. + */ + CheckTableNotInUse(matviewRel, "REFRESH MATERIALIZED VIEW"); + + relowner = matviewRel->rd_rel->relowner; + + /* + * Switch to the owner's userid, so that any functions are run as that + * user. Also arrange to make GUC variable changes local to this command. + * Don't lock it down too tight to create a temporary table just yet. We + * will switch modes when we are about to execute user code. + */ + GetUserIdAndSecContext(&save_userid, &save_sec_context); + SetUserIdAndSecContext(relowner, + save_sec_context | SECURITY_LOCAL_USERID_CHANGE); + save_nestlevel = NewGUCNestLevel(); + + tableSpace = GetDefaultTablespace(RELPERSISTENCE_TEMP, false); + relpersistence = RELPERSISTENCE_TEMP; + + /* + * Create the transient table that will receive the regenerated data. Lock + * it against access by any other process until commit (by which time it + * will be gone). + */ + if (trigdata->tg_newtable) + { + OIDDelta_new = make_new_heap(matviewOid, tableSpace, relpersistence, + ExclusiveLock); + LockRelationOid(OIDDelta_new, AccessExclusiveLock); + dest_new = CreateTransientRelDestReceiver(OIDDelta_new); + } + if (trigdata->tg_oldtable) + { + if (trigdata->tg_newtable) + OIDDelta_old = make_new_heap(OIDDelta_new, tableSpace, relpersistence, + ExclusiveLock); + else + OIDDelta_old = make_new_heap(matviewOid, tableSpace, relpersistence, + ExclusiveLock); + LockRelationOid(OIDDelta_old, AccessExclusiveLock); + dest_old = CreateTransientRelDestReceiver(OIDDelta_old); + } + + /* + * Now lock down security-restricted operations. + */ + SetUserIdAndSecContext(relowner, + save_sec_context | SECURITY_RESTRICTED_OPERATION); + + /* Generate the data. */ + if (trigdata->tg_newtable) + refresh_matview_datafill(dest_new, new_delta_qry, queryEnv, NULL); + if (trigdata->tg_oldtable) + refresh_matview_datafill(dest_old, old_delta_qry, queryEnv, NULL); + + PG_TRY(); + { + apply_delta(matviewOid, OIDDelta_new, OIDDelta_old, relowner, + save_sec_context); + } + PG_CATCH(); + { + matview_maintenance_depth = old_depth; + PG_RE_THROW(); + } + PG_END_TRY(); + + table_close(matviewRel, NoLock); + + /* Roll back any GUC changes */ + AtEOXact_GUC(false, save_nestlevel); + + /* Restore userid and security context */ + SetUserIdAndSecContext(save_userid, save_sec_context); + + return PointerGetDatum(NULL); +} + +static void +apply_delta(Oid matviewOid, Oid tempOid_new, Oid tempOid_old, + Oid relowner, int save_sec_context) +{ + StringInfoData querybuf; + StringInfoData mvatts_buf, diffatts_buf; + Relation matviewRel; + Relation tempRel_new = NULL, tempRel_old = NULL; + char *matviewname; + char *tempname_new = NULL, *tempname_old = NULL; + int i; + + + initStringInfo(&querybuf); + matviewRel = table_open(matviewOid, NoLock); + matviewname = quote_qualified_identifier(get_namespace_name(RelationGetNamespace(matviewRel)), + RelationGetRelationName(matviewRel)); + + if (OidIsValid(tempOid_new)) + { + tempRel_new = table_open(tempOid_new, NoLock); + tempname_new = quote_qualified_identifier(get_namespace_name(RelationGetNamespace(tempRel_new)), + RelationGetRelationName(tempRel_new)); + } + if (OidIsValid(tempOid_old)) + { + tempRel_old = table_open(tempOid_old, NoLock); + tempname_old = quote_qualified_identifier(get_namespace_name(RelationGetNamespace(tempRel_old)), + RelationGetRelationName(tempRel_old)); + } + + initStringInfo(&mvatts_buf); + initStringInfo(&diffatts_buf); + for (i = 0; i < matviewRel->rd_att->natts; i++) + { + Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, i); + + if (!strcmp(NameStr(attr->attname), "__ivm_count__")) + continue; + + if (i > 0) + { + appendStringInfo(&mvatts_buf, ", "); + appendStringInfo(&diffatts_buf, ", "); + } + appendStringInfo(&mvatts_buf, "mv.%s", NameStr(attr->attname)); + appendStringInfo(&diffatts_buf, "diff.%s", NameStr(attr->attname)); + } + + /* Open SPI context. */ + if (SPI_connect() != SPI_OK_CONNECT) + elog(ERROR, "SPI_connect failed"); + + /* Analyze the temp table with the new contents. */ + if (tempname_new) + { + appendStringInfo(&querybuf, "ANALYZE %s", tempname_new); + if (SPI_exec(querybuf.data, 0) != SPI_OK_UTILITY) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); + } + if (tempname_old) + { + resetStringInfo(&querybuf); + appendStringInfo(&querybuf, "ANALYZE %s", tempname_old); + if (SPI_exec(querybuf.data, 0) != SPI_OK_UTILITY) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); + } + + SetUserIdAndSecContext(relowner, + save_sec_context | SECURITY_LOCAL_USERID_CHANGE); + + OpenMatViewIncrementalMaintenance(); + + if (tempname_old) + { + resetStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH t AS (" + " SELECT diff.__ivm_count__, (diff.__ivm_count__ = mv.__ivm_count__) AS for_dlt, mv.ctid" + " FROM %s AS mv, %s AS diff WHERE (%s) = (%s)" + "), updt AS (" + " UPDATE %s AS mv SET __ivm_count__ = mv.__ivm_count__ - t.__ivm_count__" + " FROM t WHERE mv.ctid = t.ctid AND NOT for_dlt" + ") DELETE FROM %s AS mv USING t WHERE mv.ctid = t.ctid AND for_dlt;", + matviewname, tempname_old, mvatts_buf.data, diffatts_buf.data, matviewname, matviewname); + if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); + + } + if (tempname_new) + { + resetStringInfo(&querybuf); + appendStringInfo(&querybuf, + "WITH updt AS (" + " UPDATE %s AS mv SET __ivm_count__ = mv.__ivm_count__ + diff.__ivm_count__" + " FROM %s AS diff WHERE (%s) = (%s)" + " RETURNING %s" + ") INSERT INTO %s (SELECT * FROM %s AS diff WHERE (%s) NOT IN (SELECT * FROM updt));", + matviewname, tempname_new, mvatts_buf.data, diffatts_buf.data, diffatts_buf.data, matviewname, tempname_new, diffatts_buf.data); + if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); + + } + + /* We're done maintaining the materialized view. */ + CloseMatViewIncrementalMaintenance(); + + if (OidIsValid(tempOid_new)) + table_close(tempRel_new, NoLock); + if (OidIsValid(tempOid_old)) + table_close(tempRel_old, NoLock); + + table_close(matviewRel, NoLock); + + /* Clean up temp tables. */ + if (OidIsValid(tempOid_new)) + { + resetStringInfo(&querybuf); + appendStringInfo(&querybuf, "DROP TABLE %s", tempname_new); + if (SPI_exec(querybuf.data, 0) != SPI_OK_UTILITY) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); + } + if (OidIsValid(tempOid_old)) + { + resetStringInfo(&querybuf); + appendStringInfo(&querybuf, "DROP TABLE %s", tempname_old); + if (SPI_exec(querybuf.data, 0) != SPI_OK_UTILITY) + elog(ERROR, "SPI_exec failed: %s", querybuf.data); + } + + /* Close SPI context. */ + if (SPI_finish() != SPI_OK_FINISH) + elog(ERROR, "SPI_finish failed"); +} diff --git a/src/backend/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c index 78deade89b..8262ac039b 100644 --- a/src/backend/nodes/copyfuncs.c +++ b/src/backend/nodes/copyfuncs.c @@ -2361,6 +2361,7 @@ _copyRangeTblEntry(const RangeTblEntry *from) COPY_SCALAR_FIELD(relkind); COPY_SCALAR_FIELD(rellockmode); COPY_NODE_FIELD(tablesample); + COPY_SCALAR_FIELD(relisivm); COPY_NODE_FIELD(subquery); COPY_SCALAR_FIELD(security_barrier); COPY_SCALAR_FIELD(jointype); diff --git a/src/backend/nodes/equalfuncs.c b/src/backend/nodes/equalfuncs.c index 4f2ebe5118..608d477bd5 100644 --- a/src/backend/nodes/equalfuncs.c +++ b/src/backend/nodes/equalfuncs.c @@ -2641,6 +2641,7 @@ _equalRangeTblEntry(const RangeTblEntry *a, const RangeTblEntry *b) COMPARE_SCALAR_FIELD(relkind); COMPARE_SCALAR_FIELD(rellockmode); COMPARE_NODE_FIELD(tablesample); + COMPARE_SCALAR_FIELD(relisivm); COMPARE_NODE_FIELD(subquery); COMPARE_SCALAR_FIELD(security_barrier); COMPARE_SCALAR_FIELD(jointype); diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 8400dd319e..7897dd9c57 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -3042,6 +3042,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) WRITE_CHAR_FIELD(relkind); WRITE_INT_FIELD(rellockmode); WRITE_NODE_FIELD(tablesample); + WRITE_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: WRITE_NODE_FIELD(subquery); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index 6c2626ee62..aa01e205c3 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -1366,6 +1366,7 @@ _readRangeTblEntry(void) READ_CHAR_FIELD(relkind); READ_INT_FIELD(rellockmode); READ_NODE_FIELD(tablesample); + READ_BOOL_FIELD(relisivm); break; case RTE_SUBQUERY: READ_NODE_FIELD(subquery); diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index 8311b1dd46..7cae68218b 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -418,6 +418,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); %type <range> OptTempTableName %type <into> into_clause create_as_target create_mv_target +%type <boolean> incremental %type <defelt> createfunc_opt_item common_func_opt_item dostmt_opt_item %type <fun_param> func_arg func_arg_with_default table_func_column aggr_arg @@ -645,7 +646,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); HANDLER HAVING HEADER_P HOLD HOUR_P IDENTITY_P IF_P ILIKE IMMEDIATE IMMUTABLE IMPLICIT_P IMPORT_P IN_P INCLUDE - INCLUDING INCREMENT INDEX INDEXES INHERIT INHERITS INITIALLY INLINE_P + INCLUDING INCREMENT INCREMENTAL INDEX INDEXES INHERIT INHERITS INITIALLY INLINE_P INNER_P INOUT INPUT_P INSENSITIVE INSERT INSTEAD INT_P INTEGER INTERSECT INTERVAL INTO INVOKER IS ISNULL ISOLATION @@ -4054,30 +4055,32 @@ opt_with_data: *****************************************************************************/ CreateMatViewStmt: - CREATE OptNoLog MATERIALIZED VIEW create_mv_target AS SelectStmt opt_with_data + CREATE OptNoLog incremental MATERIALIZED VIEW create_mv_target AS SelectStmt opt_with_data { CreateTableAsStmt *ctas = makeNode(CreateTableAsStmt); - ctas->query = $7; - ctas->into = $5; + ctas->query = $8; + ctas->into = $6; ctas->relkind = OBJECT_MATVIEW; ctas->is_select_into = false; ctas->if_not_exists = false; /* cram additional flags into the IntoClause */ - $5->rel->relpersistence = $2; - $5->skipData = !($8); + $6->rel->relpersistence = $2; + $6->skipData = !($9); + $6->ivm = $3; $$ = (Node *) ctas; } - | CREATE OptNoLog MATERIALIZED VIEW IF_P NOT EXISTS create_mv_target AS SelectStmt opt_with_data + | CREATE OptNoLog incremental MATERIALIZED VIEW IF_P NOT EXISTS create_mv_target AS SelectStmt opt_with_data { CreateTableAsStmt *ctas = makeNode(CreateTableAsStmt); - ctas->query = $10; - ctas->into = $8; + ctas->query = $11; + ctas->into = $9; ctas->relkind = OBJECT_MATVIEW; ctas->is_select_into = false; ctas->if_not_exists = true; /* cram additional flags into the IntoClause */ - $8->rel->relpersistence = $2; - $8->skipData = !($11); + $9->rel->relpersistence = $2; + $9->skipData = !($12); + $9->ivm = $3; $$ = (Node *) ctas; } ; @@ -4094,9 +4097,14 @@ create_mv_target: $$->tableSpaceName = $5; $$->viewQuery = NULL; /* filled at analysis time */ $$->skipData = false; /* might get changed later */ + $$->ivm = false; } ; +incremental: INCREMENTAL { $$ = true; } + | /*EMPTY*/ { $$ = false; } + ; + OptNoLog: UNLOGGED { $$ = RELPERSISTENCE_UNLOGGED; } | /*EMPTY*/ { $$ = RELPERSISTENCE_PERMANENT; } ; @@ -15128,6 +15136,7 @@ unreserved_keyword: | INCLUDE | INCLUDING | INCREMENT + | INCREMENTAL | INDEX | INDEXES | INHERIT diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 77a48b039d..740ce89c34 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -37,6 +37,7 @@ #include "utils/syscache.h" #include "utils/varlena.h" +#include "commands/matview.h" #define MAX_FUZZY_DISTANCE 3 @@ -1238,6 +1239,7 @@ addRangeTableEntry(ParseState *pstate, rte->relid = RelationGetRelid(rel); rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -1317,6 +1319,7 @@ addRangeTableEntryForRelation(ParseState *pstate, rte->relid = RelationGetRelid(rel); rte->relkind = rel->rd_rel->relkind; rte->rellockmode = lockmode; + rte->relisivm = rel->rd_rel->relisivm; /* * Build the list of effective column names using user-supplied aliases @@ -2605,6 +2608,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, { Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno); + if (!strcmp("__ivm_count__", NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled()) + continue; + if (attr->attisdropped) { if (include_dropped) diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 7df2b6154c..5385a038b1 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -765,7 +765,8 @@ checkRuleResultList(List *targetList, TupleDesc resultDesc, bool isSelect, attr->atttypmod)))); } - if (i != resultDesc->natts) + /* No check for materialized views since this could have special columns for IVM */ + if ((!isSelect || requireColumnNameMatch) && i != resultDesc->natts) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), isSelect ? diff --git a/src/backend/rewrite/rewriteHandler.c b/src/backend/rewrite/rewriteHandler.c index ea40c28733..5b63c16616 100644 --- a/src/backend/rewrite/rewriteHandler.c +++ b/src/backend/rewrite/rewriteHandler.c @@ -41,6 +41,8 @@ #include "utils/lsyscache.h" #include "utils/rel.h" +#include "parser/parser.h" +#include "commands/matview.h" /* We use a list of these to detect recursion in RewriteQuery */ typedef struct rewrite_event @@ -1597,6 +1599,50 @@ ApplyRetrieveRule(Query *parsetree, if (rule->qual != NULL) elog(ERROR, "cannot handle qualified ON SELECT rule"); + if (RelationIsIVM(relation)) + { + rule_action = copyObject(linitial(rule->actions)); + + if (!rule_action->distinctClause) + { + StringInfoData str; + RawStmt *raw; + Query *sub; + + if (rule_action->hasDistinctOn) + elog(ERROR, "DISTINCT ON is not supported in IVM"); + + initStringInfo(&str); + appendStringInfo(&str, "SELECT mv.*, __ivm_count__ FROM %s mv, generate_series(1, mv.__ivm_count__)", + quote_qualified_identifier(get_namespace_name(RelationGetNamespace(relation)), + RelationGetRelationName(relation))); + + raw = (RawStmt*)linitial(raw_parser(str.data)); + sub = transformStmt(make_parsestate(NULL),raw->stmt); + + rte = rt_fetch(rt_index, parsetree->rtable); + + rte->rtekind = RTE_SUBQUERY; + rte->subquery = sub; + rte->security_barrier = RelationIsSecurityView(relation); + /* Clear fields that should not be set in a subquery RTE */ + rte->relid = InvalidOid; + rte->relkind = 0; + rte->rellockmode = 0; + rte->tablesample = NULL; + rte->inh = false; /* must not be set for a subquery */ + + rte->requiredPerms = 0; /* no permission check on subquery itself */ + rte->checkAsUser = InvalidOid; + rte->selectedCols = NULL; + rte->insertedCols = NULL; + rte->updatedCols = NULL; + rte->extraUpdatedCols = NULL; + } + + return parsetree; + } + if (rt_index == parsetree->resultRelation) { /* @@ -1906,7 +1952,8 @@ fireRIRrules(Query *parsetree, List *activeRIRs) * In that case this test would need to be postponed till after we've * opened the rel, so that we could check its state. */ - if (rte->relkind == RELKIND_MATVIEW) + if (rte->relkind == RELKIND_MATVIEW && + (!rte->relisivm || MatViewIncrementalMaintenanceIsEnabled() || parsetree->commandType != CMD_SELECT)) continue; /* diff --git a/src/backend/utils/cache/lsyscache.c b/src/backend/utils/cache/lsyscache.c index c13c08a97b..296cc53ffd 100644 --- a/src/backend/utils/cache/lsyscache.c +++ b/src/backend/utils/cache/lsyscache.c @@ -1844,6 +1844,30 @@ get_rel_relispartition(Oid relid) return false; } +/* + * get_rel_relisivm + * + * Returns the relisivm flag associated with a given relation. + */ +bool +get_rel_relisivm(Oid relid) +{ + HeapTuple tp; + + tp = SearchSysCache1(RELOID, ObjectIdGetDatum(relid)); + if (HeapTupleIsValid(tp)) + { + Form_pg_class reltup = (Form_pg_class) GETSTRUCT(tp); + bool result; + + result = reltup->relisivm; + ReleaseSysCache(tp); + return result; + } + else + return false; +} + /* * get_rel_tablespace * diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c index 2b992d7832..d80bb30696 100644 --- a/src/backend/utils/cache/relcache.c +++ b/src/backend/utils/cache/relcache.c @@ -1861,6 +1861,8 @@ formrdesc(const char *relationName, Oid relationReltype, /* ... and they're always populated, too */ relation->rd_rel->relispopulated = true; + /* ... and they're always no ivm, too */ + relation->rd_rel->relisivm = false; relation->rd_rel->relreplident = REPLICA_IDENTITY_NOTHING; relation->rd_rel->relpages = 0; diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c index 5e38f46399..d285ab6740 100644 --- a/src/bin/psql/tab-complete.c +++ b/src/bin/psql/tab-complete.c @@ -998,6 +998,7 @@ static const pgsql_thing_t words_after_create[] = { {"FOREIGN TABLE", NULL, NULL, NULL}, {"FUNCTION", NULL, NULL, Query_for_list_of_functions}, {"GROUP", Query_for_list_of_roles}, + {"INCREMENTAL MATERIALIZED VIEW", NULL, NULL, &Query_for_list_of_matviews}, {"INDEX", NULL, NULL, &Query_for_list_of_indexes}, {"LANGUAGE", Query_for_list_of_languages}, {"LARGE OBJECT", NULL, NULL, NULL, THING_NO_CREATE | THING_NO_DROP}, @@ -2482,7 +2483,7 @@ psql_completion(const char *text, int start, int end) COMPLETE_WITH("SEQUENCE", "TABLE", "VIEW"); /* Complete "CREATE UNLOGGED" with TABLE or MATVIEW */ else if (TailMatches("CREATE", "UNLOGGED")) - COMPLETE_WITH("TABLE", "MATERIALIZED VIEW"); + COMPLETE_WITH("TABLE", "MATERIALIZED VIEW", "INCREMENTAL MATERIALIZED VIEW"); /* Complete PARTITION BY with RANGE ( or LIST ( or ... */ else if (TailMatches("PARTITION", "BY")) COMPLETE_WITH("RANGE (", "LIST (", "HASH ("); @@ -2691,13 +2692,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", "INCREMENTAL", "MATERIALIZED")) COMPLETE_WITH("VIEW"); - /* Complete CREATE MATERIALIZED VIEW <name> with AS */ - else if (Matches("CREATE", "MATERIALIZED", "VIEW", MatchAny)) + /* Complete CREATE MATERIALIZED VIEW <name> with AS */ + else if (Matches("CREATE", "MATERIALIZED", "VIEW", MatchAny) || + Matches("CREATE", "INCREMENTAL", "MATERIALIZED", "VIEW", MatchAny)) COMPLETE_WITH("AS"); /* Complete "CREATE MATERIALIZED VIEW <sth> AS with "SELECT" */ - else if (Matches("CREATE", "MATERIALIZED", "VIEW", MatchAny, "AS")) + else if (Matches("CREATE", "MATERIALIZED", "VIEW", MatchAny, "AS") || + Matches("CREATE", "INCREMENTAL", "MATERIALIZED", "VIEW", MatchAny, "AS")) COMPLETE_WITH("SELECT"); /* CREATE EVENT TRIGGER */ diff --git a/src/include/catalog/pg_class.dat b/src/include/catalog/pg_class.dat index 9bcf28676d..7deda405af 100644 --- a/src/include/catalog/pg_class.dat +++ b/src/include/catalog/pg_class.dat @@ -27,7 +27,7 @@ relpersistence => 'p', relkind => 'r', relnatts => '31', relchecks => '0', relhasrules => 'f', relhastriggers => 'f', relhassubclass => 'f', relrowsecurity => 'f', relforcerowsecurity => 'f', relispopulated => 't', - relreplident => 'n', relispartition => 'f', relfrozenxid => '3', + relreplident => 'n', relispartition => 'f', relisivm => 'f', relfrozenxid => '3', relminmxid => '1', relacl => '_null_', reloptions => '_null_', relpartbound => '_null_' }, { oid => '1249', @@ -37,7 +37,7 @@ relpersistence => 'p', relkind => 'r', relnatts => '25', relchecks => '0', relhasrules => 'f', relhastriggers => 'f', relhassubclass => 'f', relrowsecurity => 'f', relforcerowsecurity => 'f', relispopulated => 't', - relreplident => 'n', relispartition => 'f', relfrozenxid => '3', + relreplident => 'n', relispartition => 'f', relisivm => 'f', relfrozenxid => '3', relminmxid => '1', relacl => '_null_', reloptions => '_null_', relpartbound => '_null_' }, { oid => '1255', @@ -47,17 +47,17 @@ relpersistence => 'p', relkind => 'r', relnatts => '29', relchecks => '0', relhasrules => 'f', relhastriggers => 'f', relhassubclass => 'f', relrowsecurity => 'f', relforcerowsecurity => 'f', relispopulated => 't', - relreplident => 'n', relispartition => 'f', relfrozenxid => '3', + relreplident => 'n', relispartition => 'f', relisivm => 'f', relfrozenxid => '3', relminmxid => '1', relacl => '_null_', reloptions => '_null_', relpartbound => '_null_' }, { oid => '1259', relname => 'pg_class', reltype => 'pg_class', relam => 'heap', relfilenode => '0', relpages => '0', reltuples => '0', relallvisible => '0', reltoastrelid => '0', relhasindex => 'f', relisshared => 'f', - relpersistence => 'p', relkind => 'r', relnatts => '33', relchecks => '0', + relpersistence => 'p', relkind => 'r', relnatts => '34', relchecks => '0', relhasrules => 'f', relhastriggers => 'f', relhassubclass => 'f', relrowsecurity => 'f', relforcerowsecurity => 'f', relispopulated => 't', - relreplident => 'n', relispartition => 'f', relfrozenxid => '3', + relreplident => 'n', relispartition => 'f', relisivm => 'f', relfrozenxid => '3', relminmxid => '1', relacl => '_null_', reloptions => '_null_', relpartbound => '_null_' }, diff --git a/src/include/catalog/pg_class.h b/src/include/catalog/pg_class.h index 090b6ba907..ff535f5504 100644 --- a/src/include/catalog/pg_class.h +++ b/src/include/catalog/pg_class.h @@ -116,6 +116,9 @@ CATALOG(pg_class,1259,RelationRelationId) BKI_BOOTSTRAP BKI_ROWTYPE_OID(83,Relat /* is relation a partition? */ bool relispartition; + /* is relation a matview with ivm? */ + bool relisivm; + /* heap for rewrite during DDL, link to original rel */ Oid relrewrite BKI_DEFAULT(0); diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 87335248a0..5de996a72c 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -10677,4 +10677,9 @@ proname => 'pg_partition_root', prorettype => 'regclass', proargtypes => 'regclass', prosrc => 'pg_partition_root' }, +# IVM +{ oid => '784', descr => 'ivm trigger', + proname => 'IVM_immediate_maintenance', provolatile => 'v', prorettype => 'trigger', + proargtypes => '', prosrc => 'IVM_immediate_maintenance' }, + ] diff --git a/src/include/commands/matview.h b/src/include/commands/matview.h index edf04bf415..8dd8193d9c 100644 --- a/src/include/commands/matview.h +++ b/src/include/commands/matview.h @@ -23,6 +23,8 @@ extern void SetMatViewPopulatedState(Relation relation, bool newstate); +extern void SetMatViewIVMState(Relation relation, bool newstate); + extern ObjectAddress ExecRefreshMatView(RefreshMatViewStmt *stmt, const char *queryString, ParamListInfo params, char *completionTag); @@ -30,4 +32,6 @@ extern DestReceiver *CreateTransientRelDestReceiver(Oid oid); extern bool MatViewIncrementalMaintenanceIsEnabled(void); +extern Datum IVM_immediate_maintenance(PG_FUNCTION_ARGS); + #endif /* MATVIEW_H */ diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 94ded3c135..5c8a5ae3ca 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1002,6 +1002,7 @@ typedef struct RangeTblEntry char relkind; /* relation kind (see pg_class.relkind) */ int rellockmode; /* lock level that query requires on the rel */ struct TableSampleClause *tablesample; /* sampling info, or NULL */ + bool relisivm; /* * Fields valid for a subquery RTE (else NULL): @@ -2059,6 +2060,7 @@ typedef struct CreateStmt char *tablespacename; /* table space to use, or NULL */ char *accessMethod; /* table access method */ bool if_not_exists; /* just do nothing if it already exists? */ + bool ivm; /* incremental view maintenance is used by materialized view */ } CreateStmt; /* ---------- diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h index 7c278c0e56..0aaef1ef15 100644 --- a/src/include/nodes/primnodes.h +++ b/src/include/nodes/primnodes.h @@ -117,6 +117,7 @@ typedef struct IntoClause char *tableSpaceName; /* table space to use, or NULL */ Node *viewQuery; /* materialized view's SELECT query */ bool skipData; /* true for WITH NO DATA */ + bool ivm; /* true for WITH IVM */ } IntoClause; diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h index 00ace8425e..d682ee11cc 100644 --- a/src/include/parser/kwlist.h +++ b/src/include/parser/kwlist.h @@ -198,6 +198,7 @@ PG_KEYWORD("in", IN_P, RESERVED_KEYWORD) PG_KEYWORD("include", INCLUDE, UNRESERVED_KEYWORD) PG_KEYWORD("including", INCLUDING, UNRESERVED_KEYWORD) PG_KEYWORD("increment", INCREMENT, UNRESERVED_KEYWORD) +PG_KEYWORD("incremental", INCREMENTAL, UNRESERVED_KEYWORD) PG_KEYWORD("index", INDEX, UNRESERVED_KEYWORD) PG_KEYWORD("indexes", INDEXES, UNRESERVED_KEYWORD) PG_KEYWORD("inherit", INHERIT, UNRESERVED_KEYWORD) diff --git a/src/include/utils/lsyscache.h b/src/include/utils/lsyscache.h index c8df5bff9f..8fd919349e 100644 --- a/src/include/utils/lsyscache.h +++ b/src/include/utils/lsyscache.h @@ -129,6 +129,7 @@ extern Oid get_rel_namespace(Oid relid); extern Oid get_rel_type_id(Oid relid); extern char get_rel_relkind(Oid relid); extern bool get_rel_relispartition(Oid relid); +extern bool get_rel_relisivm(Oid relid); extern Oid get_rel_tablespace(Oid relid); extern char get_rel_persistence(Oid relid); extern Oid get_transform_fromsql(Oid typid, Oid langid, List *trftypes); diff --git a/src/include/utils/rel.h b/src/include/utils/rel.h index d7f33abce3..057f38d5de 100644 --- a/src/include/utils/rel.h +++ b/src/include/utils/rel.h @@ -562,6 +562,8 @@ typedef struct ViewOptions */ #define RelationIsPopulated(relation) ((relation)->rd_rel->relispopulated) +#define RelationIsIVM(relation) ((relation)->rd_rel->relisivm) + /* * RelationIsAccessibleInLogicalDecoding * True if we need to log enough information to have access via