diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index dc050a76f38..27b1f16b6a8 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -2183,6 +2183,15 @@ SCRAM-SHA-256$<iteration count>:&l
+
+
+ relisivm bool
+
+
+ True if relation is incrementally maintainable materialized view
+
+
+
relrewrite oid
diff --git a/doc/src/sgml/ref/create_materialized_view.sgml b/doc/src/sgml/ref/create_materialized_view.sgml
index d8c48252f49..e0d2f86c834 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
-CREATE MATERIALIZED VIEW [ IF NOT EXISTS ] table_name
+CREATE [ INCREMENTAL ] MATERIALIZED VIEW [ IF NOT EXISTS ] table_name
[ (column_name [, ...] ) ]
[ USING method ]
[ WITH ( storage_parameter [= value] [, ... ] ) ]
@@ -60,6 +60,116 @@ CREATE MATERIALIZED VIEW [ IF NOT EXISTS ] table_name
Parameters
+
+ INCREMENTAL
+
+
+ If specified, some triggers are automatically created so that the rows
+ of the materialized view are immediately updated when base tables of the
+ materialized view are updated. In general, this allows faster update of
+ the materialized view at a price of slower update of the base tables
+ because the triggers will be invoked. We call this form of materialized
+ view as "Incrementally Maintainable Materialized View" (IMMV).
+
+
+ There are restrictions of query definitions allowed to use this
+ option. The following are supported in query definitions for IMMV:
+
+
+
+
+ Inner joins (including self-joins).
+
+
+
+
+
+ Some built-in aggregate functions (count, sum, avg, min, max) without a HAVING
+ clause.
+
+
+
+
+ Unsupported queries with this option include the following:
+
+
+
+
+ Outer joins.
+
+
+
+
+
+ Sub-queries.
+
+
+
+
+
+ Aggregate functions other than built-in count, sum, avg, min and max.
+
+
+
+
+ Aggregate functions with a HAVING clause.
+
+
+
+
+ DISTINCT ON, WINDOW, VALUES, LIMIT and OFFSET clause.
+
+
+
+
+ Other restrictions include:
+
+
+
+
+ IMMVs must be based on simple base tables. It's not supported to
+ create them on top of views or materialized views.
+
+
+
+
+
+ It is not supported to include system columns in an IMMV.
+
+CREATE INCREMENTAL MATERIALIZED VIEW mv_ivm02 AS SELECT i,j FROM mv_base_a WHERE xmin = '610';
+ERROR: system column is not supported with IVM
+
+
+
+
+
+
+ Non-immutable functions are not supported.
+
+CREATE INCREMENTAL MATERIALIZED VIEW mv_ivm12 AS SELECT i,j FROM mv_base_a WHERE i = random()::int;
+ERROR: functions in IMMV must be marked IMMUTABLE
+
+
+
+
+
+
+ IMMVs do not support expressions that contains aggregates
+
+
+
+
+
+ Logical replication does not support IMMVs.
+
+
+
+
+
+
+
+
+
IF NOT EXISTS
@@ -153,7 +263,8 @@ CREATE MATERIALIZED VIEW [ IF NOT EXISTS ] table_name
This clause specifies whether or not the materialized view should be
populated at creation time. If not, the materialized view will be
flagged as unscannable and cannot be queried until REFRESH
- MATERIALIZED VIEW is used.
+ MATERIALIZED VIEW is used. Also, if the view is IMMV,
+ triggers for maintaining the view are not created.
diff --git a/doc/src/sgml/ref/refresh_materialized_view.sgml b/doc/src/sgml/ref/refresh_materialized_view.sgml
index 675d6090f3c..c29cfc19b6c 100644
--- a/doc/src/sgml/ref/refresh_materialized_view.sgml
+++ b/doc/src/sgml/ref/refresh_materialized_view.sgml
@@ -35,9 +35,13 @@ REFRESH MATERIALIZED VIEW [ CONCURRENTLY ] name
owner of the materialized view. The old contents are discarded. If
WITH DATA is specified (or defaults) the backing query
is executed to provide the new data, and the materialized view is left in a
- scannable state. If WITH NO DATA is specified no new
+ scannable state. If the view is an incrementally maintainable materialized
+ view (IMMV) and was unpopulated, triggers for maintaining the view are
+ created. Also, a unique index is created for IMMV if it is possible and the
+ view doesn't have that yet.
+ If WITH NO DATA is specified no new
data is generated and the materialized view is left in an unscannable
- state.
+ state. If the view is IMMV, the triggers are dropped.
CONCURRENTLY and WITH NO DATA may not
diff --git a/doc/src/sgml/rules.sgml b/doc/src/sgml/rules.sgml
index 4aa4e00e017..2524ea01216 100644
--- a/doc/src/sgml/rules.sgml
+++ b/doc/src/sgml/rules.sgml
@@ -1090,6 +1090,443 @@ SELECT word FROM words ORDER BY word <-> 'caterpiler' LIMIT 10;
+
+Incremental View Maintenance
+
+
+ incremental view maintenance
+
+
+
+ materialized view
+ incremental view maintenance
+
+
+
+ view
+ incremental view maintenance
+
+
+
+Overview
+
+
+ Incremental View Maintenance (IVM) is a way to make
+ materialized views up-to-date in which only incremental changes are computed
+ and applied on views rather than recomputing the contents from scratch as
+ REFRESH MATERIALIZED VIEW does. IVM
+ can update materialized views more efficiently than recomputation when only
+ small parts of the view are changed.
+
+
+
+ There are two approaches with regard to timing of view maintenance:
+ immediate and deferred. In immediate maintenance, views are updated in the
+ same transaction that its base table is modified. In deferred maintenance,
+ views are updated after the transaction is committed, for example, when the
+ view is accessed, as a response to user command like REFRESH
+ MATERIALIZED VIEW, or periodically in background, and so on.
+ PostgreSQL currently implements only a kind of
+ immediate maintenance, in which materialized views are updated immediately
+ in AFTER triggers when a base table is modified.
+
+
+
+ To create materialized views supporting IVM, use the
+ CREATE INCREMENTAL MATERIALIZED VIEW, for example:
+
+CREATE INCREMENTAL MATERIALIZED VIEW mymatview AS SELECT * FROM mytab;
+
+ When a materialized view is created with the INCREMENTAL
+ keyword, some triggers are automatically created so that the view's contents are
+ immediately updated when its base tables are modified. We call this form
+ of materialized view an Incrementally Maintainable Materialized View
+ (IMMV).
+
+postgres=# CREATE INCREMENTAL MATERIALIZED VIEW m AS SELECT * FROM t0;
+NOTICE: could not create an index on materialized view "m" automatically
+HINT: Create an index on the materialized view for effcient incremental maintenance.
+SELECT 3
+postgres=# SELECT * FROM m;
+ i
+---
+ 1
+ 2
+ 3
+(3 rows)
+
+postgres=# INSERT INTO t0 VALUES (4);
+INSERT 0 1
+postgres=# SELECT * FROM m; -- automatically updated
+ i
+---
+ 1
+ 2
+ 3
+ 4
+(4 rows)
+
+
+
+
+ Some IMMVs have hidden columns which are added
+ automatically when a materialized view is created. Their name starts
+ with __ivm_ and they contain information required
+ for maintaining the IMMV. Such columns are not visible
+ when the IMMV is accessed by SELECT *
+ but are visible if the column name is explicitly specified in the target
+ list. We can also see the hidden columns in \d
+ meta-commands of psql commands.
+
+
+
+ In general, IMMVs allow faster updates of materialized
+ views at the price of slower updates to their base tables. Updates of
+ IMMV is slower because triggers will be invoked and the
+ view is updated in triggers per modification statement.
+
+
+
+ For example, suppose a normal materialized view defined as below:
+
+
+test=# CREATE INCREMENTAL MATERIALIZED VIEW mv_ivm AS
+ SELECT a.aid, b.bid, a.abalance, b.bbalance
+ FROM pgbench_accounts a JOIN pgbench_branches b USING(bid);
+SELECT 10000000
+
+
+
+ Updating a tuple in a base table of this materialized view is rapid but the
+ REFRESH MATERIALIZED VIEW command on this view takes a long time:
+
+
+test=# UPDATE pgbench_accounts SET abalance = 1000 WHERE aid = 1;
+UPDATE 1
+Time: 0.990 ms
+
+test=# REFRESH MATERIALIZED VIEW mv_normal ;
+REFRESH MATERIALIZED VIEW
+Time: 33533.952 ms (00:33.534)
+
+
+
+
+ On the other hand, after creating IMMV with the same view
+ definition as below:
+
+
+test=# CREATE INCREMENTAL MATERIALIZED VIEW mv_ivm AS
+ SELECT a.aid, b.bid, a.abalance, b.bbalance
+ FROM pgbench_accounts a JOIN pgbench_branches b USING(bid);
+test=# UPDATE pgbench_accounts SET abalance = 1000 WHERE aid = 1;
+NOTICE: created index "mv_ivm_index" on materialized view "mv_ivm"
+
+
+ updating a tuple in a base table takes more than the normal view,
+ but its content is updated automatically and this is faster than the
+ REFRESH MATERIALIZED VIEW command.
+
+
+test=# UPDATE pgbench_accounts SET abalance = 1000 WHERE aid = 1;
+UPDATE 1
+Time: 13.068 ms
+
+
+
+
+
+ Appropriate indexes on IMMVs are necessary for
+ efficient IVM because it looks for tuples to be
+ updated in IMMV. If there are no indexes, it
+ will take a long time.
+
+
+
+ Therefore, when IMMV is defined, a unique index is created on the view
+ automatically if possible. If the view definition query has a GROUP BY clause, a unique
+ index is created on the columns of GROUP BY expressions. Also, if the view has DISTINCT
+ clause, a unique index is created on all columns in the target list. Otherwise, if the
+ view contains all primary key attritubes of its base tables in the target list, a unique
+ index is created on these attritubes. In other cases, no index is created.
+
+
+
+ In the previous example, a unique index "mv_ivm_index" is created on aid and bid
+ columns of materialized view "mv_ivm", and this enables the rapid update of the view.
+ Dropping this index make updating the view take a loger time.
+
+test=# DROP INDEX mv_ivm_index;
+DROP INDEX
+Time: 67.081 ms
+
+test=# UPDATE pgbench_accounts SET abalance = 1000 WHERE aid = 1;
+UPDATE 1
+Time: 16386.245 ms (00:16.386)
+
+
+
+
+
+ IVM is effective when we want to keep a materialized
+ view up-to-date and small fraction of a base table is modified
+ infrequently. Due to the overhead of immediate maintenance, IVM
+ is not effective when a base table is modified frequently. Also, when a
+ large part of a base table is modified or large data is inserted into a
+ base table, IVM is not effective and the cost of
+ maintenance can be larger than the REFRESH MATERIALIZED VIEW
+ command. In such situation, we can use REFRESH MATERIALIZED VIEW
+ and specify WITH NO DATA to disable immediate
+ maintenance before modifying a base table. After a base table modification,
+ execute the REFRESH MATERIALIZED VIEW (with WITH DATA)
+ command to refresh the view data and enable immediate maintenance.
+
+
+
+
+
+Supported View Definitions and Restrictions
+
+
+ Currently, we can create IMMVs using inner joins, and some
+ aggregates. However, several restrictions apply to the definition of IMMV.
+
+
+
+Joins
+
+ Inner joins including self-join are supported. Outer joins are not supported.
+
+
+
+
+Aggregates
+
+ Supported aggregate functions are count, sum,
+ avg, min, and max.
+ Currently, only built-in aggregate functions are supported and user defined
+ aggregates cannot be used. When a base table is modified, the new aggregated
+ values are incrementally calculated using the old aggregated values and values
+ of related hidden columns stored in IMMV.
+
+
+
+ Note that for min or max, the new values
+ could be re-calculated from base tables with regard to the affected groups when a
+ tuple containing the current minimal or maximal values are deleted from a base table.
+ Therefore, it can takes a long time to update an IMMV containing
+ these functions.
+
+
+
+ Also note that using sum or avg on
+ real (float4) type or double precision
+ (float8) type in IMMV is unsafe. This is
+ because aggregated values in IMMV can become different from
+ results calculated from base tables due to the limited precision of these types.
+ To avoid this problem, use the numeric type instead.
+
+
+
+ Restrictions on Aggregates
+
+ There are the following restrictions:
+
+
+
+ If we have a GROUP BY clause, expressions specified in
+ GROUP BY must appear in the target list. This is
+ how tuples to be updated in the IMMV are identified.
+ These attributes are used as scan keys for searching tuples in the
+ IMMV, so indexes on them are required for efficient
+ IVM.
+
+
+
+
+
+ HAVING clause cannot be used.
+
+
+
+
+
+
+
+
+Other General Restrictions
+
+ There are other restrictions which generally apply to IMMV:
+
+
+
+ Sub-queries cannot be used.
+
+
+
+
+
+ CTEs cannot be used.
+
+
+
+
+
+ Window functions cannot be used.
+
+
+
+
+
+ IMMVs must be based on simple base tables. It's not
+ supported to create them on top of views, materialized views, foreign tables, inhe.
+
+
+
+
+
+ LIMIT and OFFSET clauses cannot be used.
+
+
+
+
+
+ IMMVs cannot contain system columns.
+
+
+
+
+
+ IMMVs cannot contain non-immutable functions.
+
+
+
+
+
+ UNION/INTERSECT/EXCEPT clauses cannnot be used.
+
+
+
+
+
+ DISTINCT ON clauses cannot be used.
+
+
+
+
+
+ TABLESAMPLE parameter cannot be used.
+
+
+
+
+
+ inheritance parent tables cannnot be used.
+
+
+
+
+
+ VALUES clause cannnot be used.
+
+
+
+
+
+ GROUPING SETS and FILTER clauses cannot be used.
+
+
+
+
+
+ FOR UPDATE/SHARE cannot be used.
+
+
+
+
+
+ targetlist cannot contain columns whose name start with __ivm_.
+
+
+
+
+
+ targetlist cannot contain expressions which contain an aggregate in it.
+
+
+
+
+
+ Logical replication is not supported, that is, even when a base table
+ at a publisher node is modified, IMMVs at subscriber
+ nodes are not updated.
+
+
+
+
+
+
+
+
+
+
+DISTINCT
+
+
+ PostgreSQL supports IMMV with
+ DISTINCT. For example, suppose a IMMV
+ defined with DISTINCT on a base table containing duplicate
+ tuples. When tuples are deleted from the base table, a tuple in the view is
+ deleted if and only if the multiplicity of the tuple becomes zero. Moreover,
+ when tuples are inserted into the base table, a tuple is inserted into the
+ view only if the same tuple doesn't already exist in it.
+
+
+
+ Physically, an IMMV defined with DISTINCT
+ contains tuples after eliminating duplicates, and the multiplicity of each tuple
+ is stored in a hidden column named __ivm_count__.
+
+
+
+
+Concurrent Transactions
+
+ Suppose an IMMV is defined on two base tables and each
+ table was modified in different a concurrent transaction simultaneously.
+ In the transaction which was committed first, IMMV can
+ be updated considering only the change which happened in this transaction.
+ On the other hand, in order to update the view correctly in the transaction
+ which was committed later, we need to know the changes occurred in
+ both transactions. For this reason, ExclusiveLock
+ is held on an IMMV immediately after a base table is
+ modified in READ COMMITTED mode to make sure that
+ the IMMV is updated in the latter transaction after
+ the former transaction is committed. In REPEATABLE READ
+ or SERIALIZABLE mode, an error is raised immediately
+ if lock acquisition fails because any changes which occurred in
+ other transactions are not be visible in these modes and
+ IMMV cannot be updated correctly in such situations.
+ However, as an exception if the view has only one base table and
+ INSERT is performed on the table,
+ the lock held on thew view is RowExclusiveLock.
+
+
+
+
+Row Level Security
+
+ If some base tables have row level security policy, rows that are not visible
+ to the materialized view's owner are excluded from the result. In addition, such
+ rows are excluded as well when views are incrementally maintained. However, if a
+ new policy is defined or policies are changed after the materialized view was created,
+ the new policy will not be applied to the view contents. To apply the new policy,
+ you need to refresh materialized views.
+
+
+
+
+
Rules on INSERT, UPDATE, and DELETE
diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c
index fe15e161875..5b291905edc 100644
--- a/src/backend/access/transam/xact.c
+++ b/src/backend/access/transam/xact.c
@@ -39,6 +39,7 @@
#include "catalog/storage_tablespace.h"
#include "catalog/storage_database.h"
#include "commands/async.h"
+#include "commands/matview.h"
#include "commands/dbcommands.h"
#include "commands/extension.h"
#include "commands/resgroupcmds.h"
@@ -2827,6 +2828,7 @@ CommitTransaction(void)
if ((Gp_role == GP_ROLE_DISPATCH || IS_SINGLENODE()) && IsResQueueEnabled())
AtCommit_ResScheduler();
+ AtEOXact_IVM(true);
/*
* Let ON COMMIT management do its thing (must happen after closing
* cursors, to avoid dangling-reference problems)
@@ -3156,6 +3158,8 @@ PrepareTransaction(void)
/* Shut down the deferred-trigger manager */
AfterTriggerEndXact(true);
+ /* Just after clean up triggers */
+ AtEOXact_IVM(true);
/*
* Let ON COMMIT management do its thing (must happen after closing
@@ -3565,6 +3569,7 @@ AbortTransaction(void)
AtAbort_Notify();
AtEOXact_RelationMap(false, is_parallel_worker);
AtAbort_Twophase();
+ AtAbort_IVM();
/*
* Advertise the fact that we aborted in pg_xact (assuming that we got as
@@ -6152,6 +6157,9 @@ AbortSubTransaction(void)
AbortBufferIO();
UnlockBuffers();
+ /* Clean up hash entries for incremental view maintenance */
+ AtAbort_IVM();
+
/* Reset WAL record construction state */
XLogResetInsertion();
diff --git a/src/backend/catalog/heap.c b/src/backend/catalog/heap.c
index f4d2f0612c2..c1672f56ad7 100644
--- a/src/backend/catalog/heap.c
+++ b/src/backend/catalog/heap.c
@@ -1306,6 +1306,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 917a541c6c0..b8ec6b5aa6a 100644
--- a/src/backend/catalog/index.c
+++ b/src/backend/catalog/index.c
@@ -1071,6 +1071,7 @@ index_create_internal(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 7757bd4951f..251a96a3dab 100644
--- a/src/backend/commands/createas.c
+++ b/src/backend/commands/createas.c
@@ -33,25 +33,42 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/index.h"
+#include "catalog/pg_constraint.h"
+#include "catalog/pg_inherits.h"
+#include "catalog/pg_trigger.h"
#include "catalog/toasting.h"
#include "commands/createas.h"
+#include "commands/defrem.h"
#include "commands/matview.h"
#include "commands/prepare.h"
#include "commands/tablecmds.h"
+#include "commands/tablespace.h"
+#include "commands/trigger.h"
#include "commands/view.h"
#include "miscadmin.h"
+#include "optimizer/optimizer.h"
+#include "optimizer/prep.h"
#include "nodes/makefuncs.h"
#include "nodes/nodeFuncs.h"
+#include "parser/parser.h"
+#include "parser/parsetree.h"
#include "parser/parse_clause.h"
+#include "parser/parse_func.h"
+#include "parser/parse_type.h"
#include "postmaster/autostats.h"
#include "rewrite/rewriteHandler.h"
+#include "rewrite/rewriteManip.h"
#include "storage/smgr.h"
#include "tcop/tcopprot.h"
#include "utils/builtins.h"
#include "utils/lsyscache.h"
+#include "utils/regproc.h"
+#include "utils/fmgroids.h"
#include "utils/rel.h"
#include "utils/rls.h"
#include "utils/snapmgr.h"
+#include "utils/syscache.h"
#include "catalog/oid_dispatch.h"
#include "cdb/cdbappendonlyam.h"
@@ -76,6 +93,11 @@ typedef struct
BulkInsertState bistate; /* bulk insert state */
} DR_intorel;
+typedef struct
+{
+ bool has_agg;
+} check_ivm_restriction_context;
+
static void intorel_startup_dummy(DestReceiver *self, int operation, TupleDesc typeinfo);
/* utility functions for CTAS definition creation */
static ObjectAddress create_ctas_internal(List *attrList, IntoClause *into,
@@ -87,6 +109,13 @@ static bool intorel_receive(TupleTableSlot *slot, DestReceiver *self);
static void intorel_shutdown(DestReceiver *self);
static void intorel_destroy(DestReceiver *self);
+static void CreateIvmTriggersOnBaseTablesRecurse(Query *qry, Node *node, Oid matviewOid,
+ Relids *relids, bool ex_lock);
+static void CreateIvmTrigger(Oid relOid, Oid viewOid, int16 type, int16 timing, bool ex_lock);
+static void check_ivm_restriction(Node *node);
+static bool check_ivm_restriction_walker(Node *node, check_ivm_restriction_context *context);
+static Bitmapset *get_primary_key_attnos_from_query(Query *query, List **constraintList);
+static bool check_aggregate_supports_ivm(Oid aggfnoid);
/*
* create_ctas_internal
@@ -306,6 +335,7 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt,
List *rewritten;
PlannedStmt *plan;
QueryDesc *queryDesc;
+ Query *query_immv = NULL;
Oid relationOid = InvalidOid; /* relation that is modified */
AutoStatsCmdType cmdType = AUTOSTATS_CMDTYPE_SENTINEL; /* command type */
@@ -363,6 +393,22 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt,
save_nestlevel = NewGUCNestLevel();
}
+ if (is_matview && into->ivm)
+ {
+ /* check if the query is supported in IMMV definition */
+ if (contain_mutable_functions((Node *) query))
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("mutable function is not supported on incrementally maintainable materialized view"),
+ errhint("functions must be marked IMMUTABLE")));
+
+ check_ivm_restriction((Node *) query);
+
+ /* For IMMV, we need to rewrite matview query */
+ query = rewriteQueryForIMMV(query, into->colNames);
+ query_immv = copyObject(query);
+ }
+
{
/*
* Parse analysis was done already, but we still have to run the rule
@@ -461,6 +507,34 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt,
auto_stats(cmdType, relationOid, queryDesc->es_processed, false /* inFunction */);
}
+ if (is_matview)
+ {
+ /* Roll back any GUC changes */
+ AtEOXact_GUC(false, save_nestlevel);
+
+ /* Restore userid and security context */
+ SetUserIdAndSecContext(save_userid, save_sec_context);
+
+ if (into->ivm)
+ {
+ Oid matviewOid = address.objectId;
+ Relation matviewRel = table_open(matviewOid, NoLock);
+
+ /*
+ * Mark relisivm field, if it's a matview and into->ivm is true.
+ */
+ SetMatViewIVMState(matviewRel, true);
+
+ if (!into->skipData)
+ {
+ Assert(query_immv != NULL);
+ /* Create triggers on incremental maintainable materialized view */
+ CreateIvmTriggersOnBaseTables(query_immv, matviewOid);
+ }
+ table_close(matviewRel, NoLock);
+ }
+ }
+
{
dest->rDestroy(dest);
@@ -469,16 +543,170 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt,
PopActiveSnapshot();
}
- if (is_matview)
+
+ return address;
+}
+
+/*
+ * rewriteQueryForIMMV -- rewrite view definition query for IMMV
+ *
+ * count(*) is added for counting distinct tuples in views.
+ * Also, additional hidden columns are added for aggregate values.
+ */
+Query *
+rewriteQueryForIMMV(Query *query, List *colNames)
+{
+ Query *rewritten;
+
+ Node *node;
+ ParseState *pstate = make_parsestate(NULL);
+ FuncCall *fn;
+
+ rewritten = copyObject(query);
+ pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET;
+
+ /* group keys must be in targetlist */
+ if (rewritten->groupClause)
{
- /* Roll back any GUC changes */
- AtEOXact_GUC(false, save_nestlevel);
+ ListCell *lc;
+ foreach(lc, rewritten->groupClause)
+ {
+ SortGroupClause *scl = (SortGroupClause *) lfirst(lc);
+ TargetEntry *tle = get_sortgroupclause_tle(scl, rewritten->targetList);
- /* Restore userid and security context */
- SetUserIdAndSecContext(save_userid, save_sec_context);
+ if (tle->resjunk)
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("GROUP BY expression not appearing in select list is not supported on incrementally maintainable materialized view")));
+ }
}
+ /* Convert DISTINCT to GROUP BY. count(*) will be added afterward. */
+ else if (!rewritten->hasAggs && rewritten->distinctClause)
+ rewritten->groupClause = transformDistinctClause(NULL, &rewritten->targetList, rewritten->sortClause, false);
- return address;
+ /* Add additional columns for aggregate values */
+ if (rewritten->hasAggs)
+ {
+ ListCell *lc;
+ List *aggs = NIL;
+ AttrNumber next_resno = list_length(rewritten->targetList) + 1;
+
+ foreach(lc, rewritten->targetList)
+ {
+ TargetEntry *tle = (TargetEntry *) lfirst(lc);
+ char *resname = (colNames == NIL || foreach_current_index(lc) >= list_length(colNames) ?
+ tle->resname : strVal(list_nth(colNames, tle->resno - 1)));
+
+ if (IsA(tle->expr, Aggref))
+ makeIvmAggColumn(pstate, (Aggref *) tle->expr, resname, &next_resno, &aggs);
+ }
+ rewritten->targetList = list_concat(rewritten->targetList, aggs);
+ }
+
+ /* Add count(*) for counting distinct tuples in views */
+ if (rewritten->distinctClause || rewritten->hasAggs)
+ {
+ TargetEntry *tle;
+
+ fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1);
+ fn->agg_star = true;
+
+ node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1);
+
+ tle = makeTargetEntry((Expr *) node,
+ list_length(rewritten->targetList) + 1,
+ pstrdup("__ivm_count__"),
+ false);
+ rewritten->targetList = lappend(rewritten->targetList, tle);
+ rewritten->hasAggs = true;
+ }
+
+ return rewritten;
+}
+
+/*
+ * makeIvmAggColumn -- make additional aggregate columns for IVM
+ *
+ * For an aggregate column specified by aggref, additional aggregate columns
+ * are added, which are used to calculate the new aggregate value in IMMV.
+ * An additional aggregate columns has a name based on resname
+ * (ex. ivm_count_resname), and resno specified by next_resno. The created
+ * columns are returned to aggs, and the resno for the next column is also
+ * returned to next_resno.
+ *
+ * Currently, an additional count() is created for aggref other than count.
+ * In addition, sum() is created for avg aggregate column.
+ */
+void
+makeIvmAggColumn(ParseState *pstate, Aggref *aggref, char *resname, AttrNumber *next_resno, List **aggs)
+{
+ TargetEntry *tle_count;
+ Node *node;
+ FuncCall *fn;
+ Const *dmy_arg = makeConst(INT4OID,
+ -1,
+ InvalidOid,
+ sizeof(int32),
+ Int32GetDatum(1),
+ false,
+ true); /* pass by value */
+ const char *aggname = get_func_name(aggref->aggfnoid);
+
+ /*
+ * For aggregate functions except count, add count() func with the same arg parameters.
+ * This count result is used for determining if the aggregate value should be NULL or not.
+ * Also, add sum() func for avg because we need to calculate an average value as sum/count.
+ *
+ * XXX: If there are same expressions explicitly in the target list, we can use this instead
+ * of adding new duplicated one.
+ */
+ if (strcmp(aggname, "count") != 0)
+ {
+ fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1);
+
+ /* Make a Func with a dummy arg, and then override this by the original agg's args. */
+ node = ParseFuncOrColumn(pstate, fn->funcname, list_make1(dmy_arg), NULL, fn, false, -1);
+ ((Aggref *)node)->args = aggref->args;
+
+ tle_count = makeTargetEntry((Expr *) node,
+ *next_resno,
+ pstrdup(makeObjectName("__ivm_count",resname, "_")),
+ false);
+ *aggs = lappend(*aggs, tle_count);
+ (*next_resno)++;
+ }
+ if (strcmp(aggname, "avg") == 0)
+ {
+ List *dmy_args = NIL;
+ ListCell *lc;
+ foreach(lc, aggref->aggargtypes)
+ {
+ Oid typeid = lfirst_oid(lc);
+ Type type = typeidType(typeid);
+
+ Const *con = makeConst(typeid,
+ -1,
+ typeTypeCollation(type),
+ typeLen(type),
+ (Datum) 0,
+ true,
+ typeByVal(type));
+ dmy_args = lappend(dmy_args, con);
+ ReleaseSysCache(type);
+ }
+ fn = makeFuncCall(SystemFuncName("sum"), NIL, COERCE_EXPLICIT_CALL, -1);
+
+ /* Make a Func with dummy args, and then override this by the original agg's args. */
+ node = ParseFuncOrColumn(pstate, fn->funcname, dmy_args, NULL, fn, false, -1);
+ ((Aggref *)node)->args = aggref->args;
+
+ tle_count = makeTargetEntry((Expr *) node,
+ *next_resno,
+ pstrdup(makeObjectName("__ivm_sum",resname, "_")),
+ false);
+ *aggs = lappend(*aggs, tle_count);
+ (*next_resno)++;
+ }
}
/*
@@ -623,7 +851,8 @@ intorel_initplan(struct QueryDesc *queryDesc, int eflags)
ColumnDef *col;
char *colname;
- if (lc)
+ /* Don't override hidden columns added for IVM */
+ if (lc && !isIvmName(NameStr(attribute->attname)))
{
colname = strVal(lfirst(lc));
lc = lnext(into->colNames, lc);
@@ -802,3 +1031,778 @@ GetIntoRelOid(QueryDesc *queryDesc)
else
return InvalidOid;
}
+
+/*
+ * CreateIvmTriggersOnBaseTables -- create IVM triggers on all base tables
+ */
+void
+CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid)
+{
+ Relids relids = NULL;
+ bool ex_lock = false;
+ //RangeTblEntry *rte;
+
+ /* Immediately return if we don't have any base tables. */
+ if (list_length(qry->rtable) < 1)
+ return;
+
+ /*
+ * If the view has more than one base tables, we need an exclusive lock
+ * on the view so that the view would be maintained serially to avoid
+ * the inconsistency that occurs when two base tables are modified in
+ * concurrent transactions. However, if the view has only one table,
+ * we can use a weaker lock.
+ *
+ * The type of lock should be determined here, because if we check the
+ * view definition at maintenance time, we need to acquire a weaker lock,
+ * and upgrading the lock level after this increases probability of
+ * deadlock.
+ */
+
+ // rte = list_nth(qry->rtable, 0);
+ // if (list_length(qry->rtable) > 1 || rte->rtekind != RTE_RELATION)
+ // ex_lock = true;
+ ex_lock = true;
+
+ CreateIvmTriggersOnBaseTablesRecurse(qry, (Node *)qry, matviewOid, &relids, ex_lock);
+
+ bms_free(relids);
+}
+
+static void
+CreateIvmTriggersOnBaseTablesRecurse(Query *qry, Node *node, Oid matviewOid,
+ Relids *relids, bool ex_lock)
+{
+ if (node == NULL)
+ return;
+
+ /* This can recurse, so check for excessive recursion */
+ check_stack_depth();
+
+ switch (nodeTag(node))
+ {
+ case T_Query:
+ {
+ Query *query = (Query *) node;
+
+ CreateIvmTriggersOnBaseTablesRecurse(qry, (Node *)query->jointree, matviewOid, relids, ex_lock);
+ }
+ break;
+
+ case T_RangeTblRef:
+ {
+ int rti = ((RangeTblRef *) node)->rtindex;
+ RangeTblEntry *rte = rt_fetch(rti, qry->rtable);
+
+ if (rte->rtekind == RTE_RELATION && !bms_is_member(rte->relid, *relids))
+ {
+ CreateIvmTrigger(rte->relid, matviewOid, TRIGGER_TYPE_INSERT, TRIGGER_TYPE_BEFORE, ex_lock);
+ CreateIvmTrigger(rte->relid, matviewOid, TRIGGER_TYPE_DELETE, TRIGGER_TYPE_BEFORE, ex_lock);
+ CreateIvmTrigger(rte->relid, matviewOid, TRIGGER_TYPE_UPDATE, TRIGGER_TYPE_BEFORE, ex_lock);
+ CreateIvmTrigger(rte->relid, matviewOid, TRIGGER_TYPE_TRUNCATE, TRIGGER_TYPE_BEFORE, true);
+ CreateIvmTrigger(rte->relid, matviewOid, TRIGGER_TYPE_INSERT, TRIGGER_TYPE_AFTER, ex_lock);
+ CreateIvmTrigger(rte->relid, matviewOid, TRIGGER_TYPE_DELETE, TRIGGER_TYPE_AFTER, ex_lock);
+ CreateIvmTrigger(rte->relid, matviewOid, TRIGGER_TYPE_UPDATE, TRIGGER_TYPE_AFTER, ex_lock);
+ CreateIvmTrigger(rte->relid, matviewOid, TRIGGER_TYPE_TRUNCATE, TRIGGER_TYPE_AFTER, true);
+
+ *relids = bms_add_member(*relids, rte->relid);
+ }
+ }
+ break;
+
+ case T_FromExpr:
+ {
+ FromExpr *f = (FromExpr *) node;
+ ListCell *l;
+
+ foreach(l, f->fromlist)
+ CreateIvmTriggersOnBaseTablesRecurse(qry, lfirst(l), matviewOid, relids, ex_lock);
+ }
+ break;
+
+ case T_JoinExpr:
+ {
+ JoinExpr *j = (JoinExpr *) node;
+
+ CreateIvmTriggersOnBaseTablesRecurse(qry, j->larg, matviewOid, relids, ex_lock);
+ CreateIvmTriggersOnBaseTablesRecurse(qry, j->rarg, matviewOid, relids, ex_lock);
+ }
+ break;
+
+ default:
+ elog(ERROR, "unrecognized node type: %d", (int) nodeTag(node));
+ }
+}
+
+/*
+ * CreateIvmTrigger -- create IVM trigger on a base table
+ */
+static void
+CreateIvmTrigger(Oid relOid, Oid viewOid, int16 type, int16 timing, bool ex_lock)
+{
+ ObjectAddress refaddr;
+ ObjectAddress address;
+ CreateTrigStmt *ivm_trigger;
+ List *transitionRels = NIL;
+ char internaltrigname[NAMEDATALEN];
+
+ Assert(timing == TRIGGER_TYPE_BEFORE || timing == TRIGGER_TYPE_AFTER);
+
+ refaddr.classId = RelationRelationId;
+ refaddr.objectId = viewOid;
+ refaddr.objectSubId = 0;
+
+ ivm_trigger = makeNode(CreateTrigStmt);
+ ivm_trigger->relation = makeRangeVar(get_namespace_name(get_rel_namespace(relOid)), get_rel_name(relOid), -1);
+ ivm_trigger->row = false;
+ ivm_trigger->matviewId = viewOid;
+
+ ivm_trigger->timing = timing;
+ ivm_trigger->events = type;
+
+ switch (type)
+ {
+ case TRIGGER_TYPE_INSERT:
+ ivm_trigger->trigname = (timing == TRIGGER_TYPE_BEFORE ? "IVM_trigger_ins_before" : "IVM_trigger_ins_after");
+ break;
+ case TRIGGER_TYPE_DELETE:
+ ivm_trigger->trigname = (timing == TRIGGER_TYPE_BEFORE ? "IVM_trigger_del_before" : "IVM_trigger_del_after");
+ break;
+ case TRIGGER_TYPE_UPDATE:
+ ivm_trigger->trigname = (timing == TRIGGER_TYPE_BEFORE ? "IVM_trigger_upd_before" : "IVM_trigger_upd_after");
+ break;
+ case TRIGGER_TYPE_TRUNCATE:
+ ivm_trigger->trigname = (timing == TRIGGER_TYPE_BEFORE ? "IVM_trigger_truncate_before" : "IVM_trigger_truncate_after");
+ break;
+ default:
+ elog(ERROR, "unsupported trigger type");
+ }
+ snprintf(internaltrigname, sizeof(internaltrigname),
+ "%s_%u", ivm_trigger->trigname, viewOid);
+ ivm_trigger->trigname = pstrdup(internaltrigname);
+
+ if (timing == TRIGGER_TYPE_AFTER)
+ {
+ 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);
+ }
+ }
+
+ /*
+ * XXX: When using DELETE or UPDATE, we must use exclusive lock for now
+ * because apply_old_delta(_with_count) uses ctid to identify the tuple
+ * to be deleted/deleted, but doesn't work in concurrent situations.
+ *
+ * If the view doesn't have aggregate, distinct, or tuple duplicate,
+ * then it would work even in concurrent situations. However, we don't have
+ * any way to guarantee the view has a unique key before opening the IMMV
+ * at the maintenance time because users may drop the unique index.
+ */
+
+ if (type == TRIGGER_TYPE_DELETE || type == TRIGGER_TYPE_UPDATE)
+ ex_lock = true;
+
+ ivm_trigger->funcname =
+ (timing == TRIGGER_TYPE_BEFORE ? SystemFuncName("ivm_immediate_before") : 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_make2(
+ makeString(DatumGetPointer(DirectFunctionCall1(oidout, ObjectIdGetDatum(viewOid)))),
+ makeString(DatumGetPointer(DirectFunctionCall1(boolout, BoolGetDatum(ex_lock))))
+ );
+
+ address = CreateTrigger(ivm_trigger, NULL, relOid, InvalidOid, InvalidOid,
+ InvalidOid, InvalidOid, InvalidOid, NULL, false, false);
+
+ recordDependencyOn(&address, &refaddr, DEPENDENCY_AUTO);
+
+ if (Gp_role == GP_ROLE_DISPATCH && ENABLE_DISPATCH())
+ {
+ CdbDispatchUtilityStatement((Node *) ivm_trigger,
+ DF_CANCEL_ON_ERROR|
+ DF_WITH_SNAPSHOT|
+ DF_NEED_TWO_PHASE,
+ GetAssignedOidsForDispatch(),
+ NULL);
+ }
+ /* Make changes-so-far visible */
+ CommandCounterIncrement();
+}
+
+/*
+ * check_ivm_restriction --- look for specify nodes in the query tree
+ */
+static void
+check_ivm_restriction(Node *node)
+{
+ check_ivm_restriction_context context = {false};
+
+ check_ivm_restriction_walker(node, &context);
+}
+
+static bool
+check_ivm_restriction_walker(Node *node, check_ivm_restriction_context *context)
+{
+ if (node == NULL)
+ return false;
+
+ /*
+ * We currently don't support Sub-Query.
+ */
+ if (IsA(node, SubPlan) || IsA(node, SubLink))
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("subquery is not supported on incrementally maintainable materialized view")));
+
+ /* This can recurse, so check for excessive recursion */
+ check_stack_depth();
+
+ switch (nodeTag(node))
+ {
+ case T_Query:
+ {
+ Query *qry = (Query *)node;
+ ListCell *lc;
+ List *vars;
+
+ /* if contained CTE, return error */
+ if (qry->cteList != NIL)
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("CTE is not supported on incrementally maintainable materialized view")));
+ if (qry->groupClause != NIL && !qry->hasAggs)
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("GROUP BY clause without aggregate is not supported on incrementally maintainable materialized view")));
+ if (qry->havingQual != NULL)
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg(" HAVING clause is not supported on incrementally maintainable materialized view")));
+ if (qry->sortClause != NIL) /* There is a possibility that we don't need to return an error */
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("ORDER BY clause is not supported on incrementally maintainable materialized view")));
+ if (qry->limitOffset != NULL || qry->limitCount != NULL)
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view")));
+ // if (qry->distinctClause)
+ // ereport(ERROR,
+ // (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ // errmsg("DISTINCT is not supported on incrementally maintainable materialized view")));
+ if (qry->hasDistinctOn)
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("DISTINCT ON is not supported on incrementally maintainable materialized view")));
+ if (qry->hasWindowFuncs)
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("window functions are not supported on incrementally maintainable materialized view")));
+ if (qry->groupingSets != NIL)
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("GROUPING SETS, ROLLUP, or CUBE clauses is not supported on incrementally maintainable materialized view")));
+ if (qry->setOperations != NULL)
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("UNION/INTERSECT/EXCEPT statements are not supported on incrementally maintainable materialized view")));
+ if (list_length(qry->targetList) == 0)
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("empty target list is not supported on incrementally maintainable materialized view")));
+ if (qry->rowMarks != NIL)
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("FOR UPDATE/SHARE clause is not supported on incrementally maintainable materialized view")));
+
+ /* system column restrictions */
+ vars = pull_vars_of_level((Node *) qry, 0);
+ foreach(lc, vars)
+ {
+ if (IsA(lfirst(lc), Var))
+ {
+ Var *var = (Var *) lfirst(lc);
+ /* if system column, return error */
+ if (var->varattno < 0)
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("system column is not supported on incrementally maintainable materialized view")));
+ }
+ }
+
+ context->has_agg |= qry->hasAggs;
+
+ /* restrictions for rtable */
+ foreach(lc, qry->rtable)
+ {
+ RangeTblEntry *rte = (RangeTblEntry *) lfirst(lc);
+
+ if (rte->subquery)
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("subquery is not supported on incrementally maintainable materialized view")));
+
+ if (rte->tablesample != NULL)
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("TABLESAMPLE clause is not supported on incrementally maintainable materialized view")));
+
+ if (rte->relkind == RELKIND_PARTITIONED_TABLE)
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("partitioned table is not supported on incrementally maintainable materialized view")));
+
+ if (rte->relkind == RELKIND_RELATION && has_superclass(rte->relid))
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("partitions is not supported on incrementally maintainable materialized view")));
+
+ if (rte->relkind == RELKIND_RELATION && find_inheritance_children(rte->relid, NoLock) != NIL)
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("inheritance parent is not supported on incrementally maintainable materialized view")));
+
+ if (rte->relkind == RELKIND_FOREIGN_TABLE)
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("foreign table is not supported on incrementally maintainable materialized view")));
+
+ if (rte->relkind == RELKIND_VIEW ||
+ rte->relkind == RELKIND_MATVIEW)
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("VIEW or MATERIALIZED VIEW is not supported on incrementally maintainable materialized view")));
+
+ if (rte->rtekind == RTE_VALUES)
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("VALUES is not supported on incrementally maintainable materialized view")));
+
+ if (rte->relid != InvalidOid)
+ {
+ Relation rel = table_open(rte->relid, NoLock);
+ if (RelationIsAppendOptimized(rel))
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("append-optimized table is not supported on incrementally maintainable materialized view")));
+ table_close(rel, NoLock);
+ }
+ }
+
+ query_tree_walker(qry, check_ivm_restriction_walker, (void *) context, QTW_IGNORE_RANGE_TABLE);
+
+ break;
+ }
+ case T_TargetEntry:
+ {
+ TargetEntry *tle = (TargetEntry *)node;
+ if (isIvmName(tle->resname))
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("column name %s is not supported on incrementally maintainable materialized view", tle->resname)));
+ if (context->has_agg && !IsA(tle->expr, Aggref) && contain_aggs_of_level((Node *) tle->expr, 0))
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("expression containing an aggregate in it is not supported on incrementally maintainable materialized view")));
+
+ expression_tree_walker(node, check_ivm_restriction_walker, (void *) context);
+ break;
+ }
+ case T_JoinExpr:
+ {
+ JoinExpr *joinexpr = (JoinExpr *)node;
+
+ if (joinexpr->jointype > JOIN_INNER)
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("OUTER JOIN is not supported on incrementally maintainable materialized view")));
+
+ expression_tree_walker(node, check_ivm_restriction_walker, (void *) context);
+ break;
+ }
+ case T_Aggref:
+ {
+ /* Check if this supports IVM */
+ Aggref *aggref = (Aggref *) node;
+ const char *aggname = format_procedure(aggref->aggfnoid);
+
+ if (aggref->aggfilter != NULL)
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("aggregate function with FILTER clause is not supported on incrementally maintainable materialized view")));
+
+ if (aggref->aggdistinct != NULL)
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("aggregate function with DISTINCT arguments is not supported on incrementally maintainable materialized view")));
+
+ if (aggref->aggorder != NULL)
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("aggregate function with ORDER clause is not supported on incrementally maintainable materialized view")));
+
+ if (!check_aggregate_supports_ivm(aggref->aggfnoid))
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("aggregate function %s is not supported on incrementally maintainable materialized view", aggname)));
+ break;
+ }
+ default:
+ expression_tree_walker(node, check_ivm_restriction_walker, (void *) context);
+ break;
+ }
+ return false;
+}
+
+/*
+ * check_aggregate_supports_ivm
+ *
+ * Check if the given aggregate function is supporting IVM
+ */
+static bool
+check_aggregate_supports_ivm(Oid aggfnoid)
+{
+ switch (aggfnoid)
+ {
+ /* count */
+ case F_COUNT_ANY:
+ case F_COUNT_:
+
+ /* sum */
+ case F_SUM_INT8:
+ case F_SUM_INT4:
+ case F_SUM_INT2:
+ case F_SUM_FLOAT4:
+ case F_SUM_FLOAT8:
+ case F_SUM_MONEY:
+ case F_SUM_INTERVAL:
+ case F_SUM_NUMERIC:
+
+ /* avg */
+ case F_AVG_INT8:
+ case F_AVG_INT4:
+ case F_AVG_INT2:
+ case F_AVG_NUMERIC:
+ case F_AVG_FLOAT4:
+ case F_AVG_FLOAT8:
+ case F_AVG_INTERVAL:
+
+ return true;
+
+ default:
+ return false;
+ }
+}
+
+/*
+ * CreateIndexOnIMMV
+ *
+ * Create a unique index on incremental maintainable materialized view.
+ * If the view definition query has a GROUP BY clause, the index is created
+ * on the columns of GROUP BY expressions. Otherwise, if the view contains
+ * all primary key attritubes of its base tables in the target list, the index
+ * is created on these attritubes. In other cases, no index is created.
+ */
+void
+CreateIndexOnIMMV(Query *query, Relation matviewRel)
+{
+ ListCell *lc;
+ IndexStmt *index;
+ ObjectAddress address;
+ List *constraintList = NIL;
+ char idxname[NAMEDATALEN];
+ List *indexoidlist = RelationGetIndexList(matviewRel);
+ ListCell *indexoidscan;
+
+ snprintf(idxname, sizeof(idxname), "%s_index", RelationGetRelationName(matviewRel));
+
+ index = makeNode(IndexStmt);
+
+ index->unique = true;
+ index->primary = false;
+ index->isconstraint = false;
+ index->deferrable = false;
+ index->initdeferred = false;
+ index->idxname = idxname;
+ index->relation =
+ makeRangeVar(get_namespace_name(RelationGetNamespace(matviewRel)),
+ pstrdup(RelationGetRelationName(matviewRel)),
+ -1);
+ index->accessMethod = DEFAULT_INDEX_TYPE;
+ index->options = NIL;
+ index->tableSpace = get_tablespace_name(matviewRel->rd_rel->reltablespace);
+ index->whereClause = NULL;
+ index->indexParams = NIL;
+ index->indexIncludingParams = NIL;
+ index->excludeOpNames = NIL;
+ index->idxcomment = NULL;
+ index->indexOid = InvalidOid;
+ index->oldNode = InvalidOid;
+ index->oldCreateSubid = InvalidSubTransactionId;
+ index->oldFirstRelfilenodeSubid = InvalidSubTransactionId;
+ index->transformed = true;
+ index->concurrent = false;
+ index->if_not_exists = false;
+
+ if (query->groupClause)
+ {
+ /* create unique constraint on GROUP BY expression columns */
+ foreach(lc, query->groupClause)
+ {
+ SortGroupClause *scl = (SortGroupClause *) lfirst(lc);
+ TargetEntry *tle = get_sortgroupclause_tle(scl, query->targetList);
+ Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1);
+ IndexElem *iparam;
+
+ iparam = makeNode(IndexElem);
+ iparam->name = pstrdup(NameStr(attr->attname));
+ iparam->expr = NULL;
+ iparam->indexcolname = NULL;
+ iparam->collation = NIL;
+ iparam->opclass = NIL;
+ iparam->opclassopts = NIL;
+ iparam->ordering = SORTBY_DEFAULT;
+ iparam->nulls_ordering = SORTBY_NULLS_DEFAULT;
+ index->indexParams = lappend(index->indexParams, iparam);
+ }
+ }
+ else if (query->distinctClause)
+ {
+ /* create unique constraint on all columns */
+ foreach(lc, query->targetList)
+ {
+ TargetEntry *tle = (TargetEntry *) lfirst(lc);
+ Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1);
+ IndexElem *iparam;
+
+ iparam = makeNode(IndexElem);
+ iparam->name = pstrdup(NameStr(attr->attname));
+ iparam->expr = NULL;
+ iparam->indexcolname = NULL;
+ iparam->collation = NIL;
+ iparam->opclass = NIL;
+ iparam->opclassopts = NIL;
+ iparam->ordering = SORTBY_DEFAULT;
+ iparam->nulls_ordering = SORTBY_NULLS_DEFAULT;
+ index->indexParams = lappend(index->indexParams, iparam);
+ }
+ }
+ else
+ {
+ Bitmapset *key_attnos;
+
+ /* create index on the base tables' primary key columns */
+ key_attnos = get_primary_key_attnos_from_query(query, &constraintList);
+ if (key_attnos)
+ {
+ foreach(lc, query->targetList)
+ {
+ TargetEntry *tle = (TargetEntry *) lfirst(lc);
+ Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1);
+
+ if (bms_is_member(tle->resno - FirstLowInvalidHeapAttributeNumber, key_attnos))
+ {
+ IndexElem *iparam;
+
+ iparam = makeNode(IndexElem);
+ iparam->name = pstrdup(NameStr(attr->attname));
+ iparam->expr = NULL;
+ iparam->indexcolname = NULL;
+ iparam->collation = NIL;
+ iparam->opclass = NIL;
+ iparam->opclassopts = NIL;
+ iparam->ordering = SORTBY_DEFAULT;
+ iparam->nulls_ordering = SORTBY_NULLS_DEFAULT;
+ index->indexParams = lappend(index->indexParams, iparam);
+ }
+ }
+ }
+ else
+ {
+ /* create no index, just notice that an appropriate index is necessary for efficient IVM */
+ ereport(NOTICE,
+ (errmsg("could not create an index on materialized view \"%s\" automatically",
+ RelationGetRelationName(matviewRel)),
+ errdetail("This target list does not have all the primary key columns, "
+ "or this view does not contain GROUP BY or DISTINCT clause."),
+ errhint("Create an index on the materialized view for efficient incremental maintenance.")));
+ return;
+ }
+ }
+
+ /* If we have a compatible index, we don't need to create another. */
+ foreach(indexoidscan, indexoidlist)
+ {
+ Oid indexoid = lfirst_oid(indexoidscan);
+ Relation indexRel;
+ bool hasCompatibleIndex = false;
+
+ indexRel = index_open(indexoid, AccessShareLock);
+
+ if (CheckIndexCompatible(indexRel->rd_id,
+ index->accessMethod,
+ index->indexParams,
+ index->excludeOpNames))
+ hasCompatibleIndex = true;
+
+ index_close(indexRel, AccessShareLock);
+
+ if (hasCompatibleIndex)
+ return;
+ }
+
+ address = DefineIndex(RelationGetRelid(matviewRel),
+ index,
+ InvalidOid,
+ InvalidOid,
+ InvalidOid,
+ false, true, false, false, true);
+
+ ereport(NOTICE,
+ (errmsg("created index \"%s\" on materialized view \"%s\"",
+ idxname, RelationGetRelationName(matviewRel))));
+
+ /*
+ * Make dependencies so that the index is dropped if any base tables's
+ * primary key is dropped.
+ */
+ foreach(lc, constraintList)
+ {
+ Oid constraintOid = lfirst_oid(lc);
+ ObjectAddress refaddr;
+
+ refaddr.classId = ConstraintRelationId;
+ refaddr.objectId = constraintOid;
+ refaddr.objectSubId = 0;
+
+ recordDependencyOn(&address, &refaddr, DEPENDENCY_NORMAL);
+ }
+}
+
+
+/*
+ * get_primary_key_attnos_from_query
+ *
+ * Identify the columns in base tables' primary keys in the target list.
+ *
+ * Returns a Bitmapset of the column attnos of the primary key's columns of
+ * tables that used in the query. The attnos are offset by
+ * FirstLowInvalidHeapAttributeNumber as same as get_primary_key_attnos.
+ *
+ * If any table has no primary key or any primary key's columns is not in
+ * the target list, return NULL. We also return NULL if any pkey constraint
+ * is deferrable.
+ *
+ * constraintList is set to a list of the OIDs of the pkey constraints.
+ */
+static Bitmapset *
+get_primary_key_attnos_from_query(Query *query, List **constraintList)
+{
+ List *key_attnos_list = NIL;
+ ListCell *lc;
+ int i;
+ Bitmapset *keys = NULL;
+ Relids rels_in_from;
+
+ /*
+ * Collect primary key attributes from all tables used in query. The key attributes
+ * sets for each table are stored in key_attnos_list in order by RTE index.
+ */
+ foreach(lc, query->rtable)
+ {
+ RangeTblEntry *r = (RangeTblEntry*) lfirst(lc);
+ Bitmapset *key_attnos;
+ bool has_pkey = true;
+
+ /* for tables, call get_primary_key_attnos */
+ if (r->rtekind == RTE_RELATION)
+ {
+ Oid constraintOid;
+ key_attnos = get_primary_key_attnos(r->relid, false, &constraintOid);
+ *constraintList = lappend_oid(*constraintList, constraintOid);
+ has_pkey = (key_attnos != NULL);
+ }
+ /* for other RTEs, store NULL into key_attnos_list */
+ else
+ key_attnos = NULL;
+
+ /*
+ * If any table or subquery has no primary key or its pkey constraint is deferrable,
+ * we cannot get key attributes for this query, so return NULL.
+ */
+ if (!has_pkey)
+ return NULL;
+
+ key_attnos_list = lappend(key_attnos_list, key_attnos);
+ }
+
+ /* Collect key attributes appearing in the target list */
+ i = 1;
+ foreach(lc, query->targetList)
+ {
+ TargetEntry *tle = (TargetEntry *) flatten_join_alias_vars(query, lfirst(lc));
+
+ if (IsA(tle->expr, Var))
+ {
+ Var *var = (Var*) tle->expr;
+ Bitmapset *key_attnos = list_nth(key_attnos_list, var->varno - 1);
+
+ /* check if this attribute is from a base table's primary key */
+ if (bms_is_member(var->varattno - FirstLowInvalidHeapAttributeNumber, key_attnos))
+ {
+ /*
+ * Remove found key attributes from key_attnos_list, and add this
+ * to the result list.
+ */
+ key_attnos = bms_del_member(key_attnos, var->varattno - FirstLowInvalidHeapAttributeNumber);
+ if (bms_is_empty(key_attnos))
+ {
+ key_attnos_list = list_delete_nth_cell(key_attnos_list, var->varno - 1);
+ key_attnos_list = list_insert_nth(key_attnos_list, var->varno - 1, NULL);
+ }
+ keys = bms_add_member(keys, i - FirstLowInvalidHeapAttributeNumber);
+ }
+ }
+ i++;
+ }
+
+ /* Collect RTE indexes of relations appearing in the FROM clause */
+ rels_in_from = get_relids_in_jointree((Node *) query->jointree, false);
+
+ /*
+ * Check if all key attributes of relations in FROM are appearing in the target
+ * list. If an attribute remains in key_attnos_list in spite of the table is used
+ * in FROM clause, the target is missing this key attribute, so we return NULL.
+ */
+ i = 1;
+ foreach(lc, key_attnos_list)
+ {
+ Bitmapset *bms = (Bitmapset *)lfirst(lc);
+ if (!bms_is_empty(bms) && bms_is_member(i, rels_in_from))
+ return NULL;
+ i++;
+ }
+
+ return keys;
+}
diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c
index 51e30960223..5aed99c7ba3 100644
--- a/src/backend/commands/indexcmds.c
+++ b/src/backend/commands/indexcmds.c
@@ -40,6 +40,7 @@
#include "commands/dbcommands.h"
#include "commands/defrem.h"
#include "commands/event_trigger.h"
+#include "commands/matview.h"
#include "commands/progress.h"
#include "commands/tablecmds.h"
#include "commands/tablespace.h"
@@ -1153,6 +1154,43 @@ DefineIndex(Oid relationId,
accessMethodName, accessMethodId,
amcanorder, stmt->isconstraint);
+ /*
+ * We disallow unique indexes on IVM columns of IMMVs.
+ */
+ if (RelationIsIVM(rel) && stmt->unique)
+ {
+ for (int i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++)
+ {
+ AttrNumber attno = indexInfo->ii_IndexAttrNumbers[i];
+ if (attno > 0)
+ {
+ char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname);
+ if (name && isIvmName(name))
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("unique index creation on IVM columns is not supported")));
+ }
+ }
+
+ if (indexInfo->ii_Expressions)
+ {
+ Bitmapset *indexattrs = NULL;
+ int varno = -1;
+
+ pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &indexattrs);
+
+ while ((varno = bms_next_member(indexattrs, varno)) >= 0)
+ {
+ int attno = varno + FirstLowInvalidHeapAttributeNumber;
+ char *name = NameStr(TupleDescAttr(rel->rd_att, attno - 1)->attname);
+ if (name && isIvmName(name))
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("unique index creation on IVM columns is not supported")));
+ }
+
+ }
+ }
/*
* Extra checks when creating a PRIMARY KEY index.
*/
diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c
index b1f6306f681..2af0817a456 100644
--- a/src/backend/commands/matview.c
+++ b/src/backend/commands/matview.c
@@ -27,21 +27,36 @@
#include "catalog/namespace.h"
#include "catalog/oid_dispatch.h"
#include "catalog/pg_am.h"
+#include "catalog/pg_depend.h"
+#include "catalog/pg_trigger.h"
#include "catalog/pg_opclass.h"
#include "catalog/pg_operator.h"
#include "cdb/cdbaocsam.h"
#include "cdb/cdbappendonlyam.h"
#include "cdb/cdbvars.h"
#include "commands/cluster.h"
+#include "commands/createas.h"
+#include "commands/defrem.h"
#include "commands/matview.h"
#include "commands/tablecmds.h"
#include "commands/tablespace.h"
+#include "common/hashfn.h"
#include "executor/executor.h"
+#include "executor/nodeShareInputScan.h"
#include "executor/spi.h"
+#include "executor/tstoreReceiver.h"
#include "miscadmin.h"
+#include "nodes/makefuncs.h"
+#include "optimizer/optimizer.h"
+#include "parser/analyze.h"
+#include "parser/parse_clause.h"
+#include "parser/parse_func.h"
#include "parser/parse_relation.h"
+#include "parser/parsetree.h"
#include "pgstat.h"
#include "rewrite/rewriteHandler.h"
+#include "rewrite/rowsecurity.h"
+#include "storage/proc.h"
#include "storage/lmgr.h"
#include "storage/smgr.h"
#include "tcop/tcopprot.h"
@@ -50,6 +65,7 @@
#include "utils/rel.h"
#include "utils/snapmgr.h"
#include "utils/syscache.h"
+#include "utils/typcache.h"
typedef struct
@@ -68,15 +84,89 @@ typedef struct
uint64 processed; /* GPDB: number of tuples inserted */
} DR_transientrel;
+#define MV_INIT_QUERYHASHSIZE 32
+#define MV_INIT_SNAPSHOTHASHSIZE (2 * MaxBackends)
+#define SNAPSHOT_KEYSIZE 128
+
+/*
+ * MV_TriggerHashEntry
+ *
+ * Hash entry for base tables on which IVM trigger is invoked
+ */
+typedef struct MV_TriggerHashEntry
+{
+ Oid matview_id; /* OID of the materialized view */
+ int before_trig_count; /* count of before triggers invoked */
+ int after_trig_count; /* count of after triggers invoked */
+ int pid; /* for debug */
+ int reference; /* reference count */
+
+ Snapshot snapshot; /* Snapshot just before table change */
+ char *snapname; /* Snapshot name for lookup */
+
+ List *tables; /* List of MV_TriggerTable */
+ bool has_old; /* tuples are deleted from any table? */
+ bool has_new; /* tuples are inserted into any table? */
+ MemoryContext context; /* The session-scoped memory context. */
+ ResourceOwner resowner; /* The session-scoped resource owner. */
+} MV_TriggerHashEntry;
+
+/* SnapshotDumpEntry to hold information about a snapshot dump entry */
+typedef struct SnapshotDumpEntry
+{
+ char snapname[SNAPSHOT_KEYSIZE]; /* Name of the snapshot */
+ Oid matview_id; /* OID of the materialized view */
+ int pid; /* Process ID of creater */
+ dsm_handle handle; /* Handle to the DSM segment */
+ dsm_segment *segment; /* Pointer to the DSM segment */
+} SnapshotDumpEntry;
+
+/*
+ * MV_TriggerTable
+ *
+ * IVM related data for tables on which the trigger is invoked.
+ */
+typedef struct MV_TriggerTable
+{
+ Oid table_id; /* OID of the modified table */
+ List *old_tuplestores; /* tuplestores for deleted tuples */
+ List *new_tuplestores; /* tuplestores for inserted tuples */
+
+ List *rte_indexes; /* List of RTE index of the modified table */
+ RangeTblEntry *original_rte; /* the original RTE saved before rewriting query */
+
+ Relation rel; /* relation of the modified table */
+ TupleTableSlot *slot; /* for checking visibility in the pre-state table */
+} MV_TriggerTable;
+
+static HTAB *mv_trigger_info = NULL;
+static HTAB *mv_trigger_snapshot = NULL;
+
+/* kind of IVM operation for the view */
+typedef enum
+{
+ IVM_ADD,
+ IVM_SUB
+} IvmOp;
+
+/* ENR name for materialized view delta */
+#define NEW_DELTA_ENRNAME "new_delta"
+#define OLD_DELTA_ENRNAME "old_delta"
+
static int matview_maintenance_depth = 0;
static RefreshClause* MakeRefreshClause(bool concurrent, bool skipData, RangeVar *relation);
+static IntoClause* makeIvmIntoClause(const char *enrname, Relation matviewRel);
static void transientrel_startup(DestReceiver *self, int operation, TupleDesc typeinfo);
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, RefreshClause *refreshClause);
+static uint64 refresh_matview_memoryfill(DestReceiver *dest,Query *query,
+ QueryEnvironment *queryEnv,
+ TupleDesc *resultTupleDesc,
+ const char *queryString, Relation matviewRel);
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);
@@ -84,7 +174,63 @@ static void refresh_by_heap_swap(Oid matviewOid, Oid OIDNewHeap, char relpersist
static bool is_usable_unique_index(Relation indexRel);
static void OpenMatViewIncrementalMaintenance(void);
static void CloseMatViewIncrementalMaintenance(void);
-
+static Query *get_matview_query(Relation matviewRel);
+
+static Query *rewrite_query_for_preupdate_state(Query *query, List *tables,
+ ParseState *pstate, Oid matviewid);
+static void register_delta_ENRs(ParseState *pstate, Query *query, List *tables);
+static char *make_delta_enr_name(const char *prefix, Oid relid, int count);
+static RangeTblEntry *get_prestate_rte(RangeTblEntry *rte, MV_TriggerTable *table,
+ QueryEnvironment *queryEnv, Oid matviewid);
+static RangeTblEntry *replace_rte_with_delta(RangeTblEntry *rte, MV_TriggerTable *table, bool is_new,
+ QueryEnvironment *queryEnv);
+static Query *rewrite_query_for_counting_and_aggregates(Query *query, ParseState *pstate);
+
+static char* calc_delta_old(Tuplestorestate *ts,Relation matviewRel, MV_TriggerTable *table, int rte_index, Query *query,
+ DestReceiver *dest_old,
+ TupleDesc *tupdesc_old,
+ QueryEnvironment *queryEnv);
+static char* calc_delta_new(Tuplestorestate *ts, Relation matviewRel, MV_TriggerTable *table, int rte_index, Query *query,
+ DestReceiver *dest_new,
+ TupleDesc *tupdesc_new,
+ QueryEnvironment *queryEnv);
+static Query *rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte_index);
+
+static void apply_delta(char *old_enr, char *new_enr, MV_TriggerTable *table, Oid matviewOid, Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores,
+ TupleDesc tupdesc_old, TupleDesc tupdesc_new,
+ Query *query, bool use_count, char *count_colname);
+static void append_set_clause_for_count(const char *resname, StringInfo buf_old,
+ StringInfo buf_new,StringInfo aggs_list);
+static void append_set_clause_for_sum(const char *resname, StringInfo buf_old,
+ StringInfo buf_new, StringInfo aggs_list);
+static void append_set_clause_for_avg(const char *resname, StringInfo buf_old,
+ StringInfo buf_new, StringInfo aggs_list,
+ const char *aggtype);
+static char *get_operation_string(IvmOp op, const char *col, const char *arg1, const char *arg2,
+ const char* count_col, const char *castType);
+static char *get_null_condition_string(IvmOp op, const char *arg1, const char *arg2,
+ const char* count_col);
+static void apply_old_delta(const char *matviewname, const char *deltaname_old,
+ List *keys);
+static void apply_old_delta_with_count(const char *matviewname, const char *deltaname_old,
+ List *keys, StringInfo aggs_list, StringInfo aggs_set,
+ const char *count_colname);
+static void apply_new_delta(const char *matviewname, const char *deltaname_new,
+ StringInfo target_list);
+static void apply_new_delta_with_count(const char *matviewname, const char* deltaname_new,
+ List *keys, StringInfo target_list, StringInfo aggs_set,
+ const char* count_colname);
+static char *get_matching_condition_string(List *keys);
+static void generate_equal(StringInfo querybuf, Oid opttype,
+ const char *leftop, const char *rightop);
+
+static void clean_up_IVM_hash_entry(MV_TriggerHashEntry *entry, bool is_abort);
+static void clean_up_ivm_dsm_entry(MV_TriggerHashEntry *entry);
+static void apply_cleanup(Oid matview_id);
+static void ivm_export_snapshot(Oid matview_id, char *snapname);
+static Snapshot ivm_import_snapshot(const char *idstr);
+static void ExecuteTruncateGuts_IVM(Relation matviewRel, Oid matviewOid, Query *query);
+static void ivm_set_ts_persitent_name(TriggerData *trigdata, Oid relid, Oid mvid);
/*
* SetMatViewPopulatedState
* Mark a materialized view as populated, or not.
@@ -138,6 +284,46 @@ MakeRefreshClause(bool concurrent, bool skipData, RangeVar *relation)
return refreshClause;
}
+/*
+ * 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
*
@@ -164,9 +350,8 @@ ExecRefreshMatView(RefreshMatViewStmt *stmt, const char *queryString,
{
Oid matviewOid;
Relation matviewRel;
- RewriteRule *rule;
- List *actions;
Query *dataQuery;
+ Query *viewQuery;
Oid tableSpace;
Oid relowner;
Oid OIDNewHeap;
@@ -180,6 +365,7 @@ ExecRefreshMatView(RefreshMatViewStmt *stmt, const char *queryString,
int save_nestlevel;
ObjectAddress address;
RefreshClause *refreshClause;
+ bool oldPopulated;
/* MATERIALIZED_VIEW_FIXME: Refresh MatView is not MPP-fied. */
@@ -205,6 +391,7 @@ ExecRefreshMatView(RefreshMatViewStmt *stmt, const char *queryString,
SetUserIdAndSecContext(relowner,
save_sec_context | SECURITY_RESTRICTED_OPERATION);
save_nestlevel = NewGUCNestLevel();
+ oldPopulated = RelationIsPopulated(matviewRel);
/* Make sure it is a materialized view. */
if (matviewRel->rd_rel->relkind != RELKIND_MATVIEW)
@@ -226,32 +413,13 @@ ExecRefreshMatView(RefreshMatViewStmt *stmt, const char *queryString,
errmsg("%s and %s options cannot be used together",
"CONCURRENTLY", "WITH NO DATA")));
- /*
- * Check that everything is correct for a refresh. Problems at this point
- * are internal errors, so elog is sufficient.
- */
- if (matviewRel->rd_rel->relhasrules == false ||
- matviewRel->rd_rules->numLocks < 1)
- elog(ERROR,
- "materialized view \"%s\" is missing rewrite information",
- RelationGetRelationName(matviewRel));
-
- if (matviewRel->rd_rules->numLocks > 1)
- elog(ERROR,
- "materialized view \"%s\" has too many rules",
- RelationGetRelationName(matviewRel));
-
- rule = matviewRel->rd_rules->rules[0];
- if (rule->event != CMD_SELECT || !(rule->isInstead))
- elog(ERROR,
- "the rule for materialized view \"%s\" is not a SELECT INSTEAD OF rule",
- RelationGetRelationName(matviewRel));
+ viewQuery = get_matview_query(matviewRel);
- actions = rule->actions;
- if (list_length(actions) != 1)
- elog(ERROR,
- "the rule for materialized view \"%s\" is not a single action",
- RelationGetRelationName(matviewRel));
+ /* For IMMV, we need to rewrite matview query */
+ if (!stmt->skipData && RelationIsIVM(matviewRel))
+ dataQuery = rewriteQueryForIMMV(viewQuery,NIL);
+ else
+ dataQuery = viewQuery;
/*
* Check that there is a unique index with no WHERE clause on one or more
@@ -312,7 +480,6 @@ ExecRefreshMatView(RefreshMatViewStmt *stmt, const char *queryString,
* is not equal to newRel->rule(parentStmtType = PARENTSTMTTYPE_NONE),
* caused oldRel->rule(dataQuery) to be released
*/
- dataQuery = copyObject(linitial_node(Query, actions));
Assert(IsA(dataQuery, Query));
dataQuery->parentStmtType = PARENTSTMTTYPE_REFRESH_MATVIEW;
@@ -329,6 +496,74 @@ ExecRefreshMatView(RefreshMatViewStmt *stmt, const char *queryString,
relpersistence = matviewRel->rd_rel->relpersistence;
}
+ /* delete IMMV triggers. */
+ if (RelationIsIVM(matviewRel) && stmt->skipData)
+ {
+ Relation tgRel;
+ Relation depRel;
+ ScanKeyData key;
+ SysScanDesc scan;
+ HeapTuple tup;
+ ObjectAddresses *immv_triggers;
+
+ immv_triggers = new_object_addresses();
+
+ tgRel = table_open(TriggerRelationId, RowExclusiveLock);
+ depRel = table_open(DependRelationId, RowExclusiveLock);
+
+ /* search triggers that depends on IMMV. */
+ ScanKeyInit(&key,
+ Anum_pg_depend_refobjid,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(matviewOid));
+ scan = systable_beginscan(depRel, DependReferenceIndexId, true,
+ NULL, 1, &key);
+ while ((tup = systable_getnext(scan)) != NULL)
+ {
+ ObjectAddress obj;
+ Form_pg_depend foundDep = (Form_pg_depend) GETSTRUCT(tup);
+
+ if (foundDep->classid == TriggerRelationId)
+ {
+ HeapTuple tgtup;
+ ScanKeyData tgkey[1];
+ SysScanDesc tgscan;
+ Form_pg_trigger tgform;
+
+ /* Find the trigger name. */
+ ScanKeyInit(&tgkey[0],
+ Anum_pg_trigger_oid,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(foundDep->objid));
+
+ tgscan = systable_beginscan(tgRel, TriggerOidIndexId, true,
+ NULL, 1, tgkey);
+ tgtup = systable_getnext(tgscan);
+ if (!HeapTupleIsValid(tgtup))
+ elog(ERROR, "could not find tuple for immv trigger %u", foundDep->objid);
+
+ tgform = (Form_pg_trigger) GETSTRUCT(tgtup);
+
+ /* If trigger is created by IMMV, delete it. */
+ if (strncmp(NameStr(tgform->tgname), "IVM_trigger_", 12) == 0)
+ {
+ obj.classId = foundDep->classid;
+ obj.objectId = foundDep->objid;
+ obj.objectSubId = foundDep->refobjsubid;
+ add_exact_object_address(&obj, immv_triggers);
+ }
+ systable_endscan(tgscan);
+ }
+ }
+ systable_endscan(scan);
+
+ performMultipleDeletions(immv_triggers, DROP_RESTRICT, PERFORM_DELETION_INTERNAL);
+
+ table_close(depRel, RowExclusiveLock);
+ table_close(tgRel, RowExclusiveLock);
+ free_object_addresses(immv_triggers);
+ }
+
/*
* Create the transient table that will receive the regenerated data. Lock
* it against access by any other process until commit (by which time it
@@ -397,6 +632,11 @@ ExecRefreshMatView(RefreshMatViewStmt *stmt, const char *queryString,
// pgstat_count_heap_insert(matviewRel, processed);
}
+ if (!stmt->skipData && RelationIsIVM(matviewRel) && !oldPopulated)
+ {
+ CreateIvmTriggersOnBaseTables(dataQuery, matviewOid);
+ }
+
table_close(matviewRel, NoLock);
/* Roll back any GUC changes */
@@ -512,6 +752,8 @@ refresh_matview_datafill(DestReceiver *dest, Query *query,
ExecutorFinish(queryDesc);
ExecutorEnd(queryDesc);
+ processed = queryDesc->es_processed;
+
FreeQueryDesc(queryDesc);
PopActiveSnapshot();
@@ -1128,3 +1370,2471 @@ CloseMatViewIncrementalMaintenance(void)
matview_maintenance_depth--;
Assert(matview_maintenance_depth >= 0);
}
+
+/*
+ * get_matview_query - get the Query from a matview's _RETURN rule.
+ */
+static Query *
+get_matview_query(Relation matviewRel)
+{
+ RewriteRule *rule;
+ List * actions;
+
+ /*
+ * Check that everything is correct for a refresh. Problems at this point
+ * are internal errors, so elog is sufficient.
+ */
+ if (matviewRel->rd_rel->relhasrules == false ||
+ matviewRel->rd_rules->numLocks < 1)
+ elog(ERROR,
+ "materialized view \"%s\" is missing rewrite information",
+ RelationGetRelationName(matviewRel));
+
+ if (matviewRel->rd_rules->numLocks > 1)
+ elog(ERROR,
+ "materialized view \"%s\" has too many rules",
+ RelationGetRelationName(matviewRel));
+
+ rule = matviewRel->rd_rules->rules[0];
+ if (rule->event != CMD_SELECT || !(rule->isInstead))
+ elog(ERROR,
+ "the rule for materialized view \"%s\" is not a SELECT INSTEAD OF rule",
+ RelationGetRelationName(matviewRel));
+
+ actions = rule->actions;
+ if (list_length(actions) != 1)
+ elog(ERROR,
+ "the rule for materialized view \"%s\" is not a single action",
+ RelationGetRelationName(matviewRel));
+
+ /*
+ * The stored query was rewritten at the time of the MV definition, but
+ * has not been scribbled on by the planner.
+ */
+ return linitial_node(Query, actions);
+}
+
+
+/* ----------------------------------------------------
+ * Incremental View Maintenance routines
+ * ---------------------------------------------------
+ */
+
+/*
+ * ivm_immediate_before
+ *
+ * IVM trigger function invoked before base table is modified. If this is
+ * invoked firstly in the same statement, we save the transaction id and the
+ * command id at that time.
+ */
+Datum
+ivm_immediate_before(PG_FUNCTION_ARGS)
+{
+ TriggerData *trigdata = (TriggerData *) fcinfo->context;
+ char *matviewOid_text = trigdata->tg_trigger->tgargs[0];
+ char *ex_lock_text = trigdata->tg_trigger->tgargs[1];
+ Oid matviewOid;
+ MV_TriggerHashEntry *entry;
+ bool found;
+ bool ex_lock;
+ ResourceOwner oldowner;
+ MemoryContext oldctx;
+ Relation rel = trigdata->tg_relation;
+
+ matviewOid = DatumGetObjectId(DirectFunctionCall1(oidin, CStringGetDatum(matviewOid_text)));
+ ex_lock = DatumGetBool(DirectFunctionCall1(boolin, CStringGetDatum(ex_lock_text)));
+
+ /* If the view has more than one tables, we have to use an exclusive lock. */
+ if (ex_lock)
+ {
+ /*
+ * Wait for concurrent transactions which update this materialized view at
+ * READ COMMITED. This is needed to see changes committed in other
+ * transactions. No wait and raise an error at REPEATABLE READ or
+ * SERIALIZABLE to prevent update anomalies of matviews.
+ * XXX: dead-lock is possible here.
+ */
+ if (!IsolationUsesXactSnapshot())
+ LockRelationOid(matviewOid, ExclusiveLock);
+ else if (!ConditionalLockRelationOid(matviewOid, ExclusiveLock))
+ {
+ /* try to throw error by name; relation could be deleted... */
+ char *relname = get_rel_name(matviewOid);
+
+ if (!relname)
+ ereport(ERROR,
+ (errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+ errmsg("could not obtain lock on materialized view during incremental maintenance")));
+
+ ereport(ERROR,
+ (errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+ errmsg("could not obtain lock on materialized view \"%s\" during incremental maintenance",
+ relname)));
+ }
+ }
+ else
+ LockRelationOid(matviewOid, RowExclusiveLock);
+
+ entry = (MV_TriggerHashEntry *) hash_search(mv_trigger_info,
+ (void *) &matviewOid,
+ HASH_ENTER, &found);
+
+ /* On the first BEFORE to update the view, initialize trigger data */
+ if (!found)
+ {
+ /*
+ * Get a snapshot just before the table was modified for checking
+ * tuple visibility in the pre-update state of the table.
+ */
+ entry->context = AllocSetContextCreate(TopMemoryContext,
+ "IVM Writer Session",
+ ALLOCSET_DEFAULT_SIZES);
+ entry->resowner = ResourceOwnerCreate(TopTransactionResourceOwner, "IVM Writer Session");
+ /* Change to session owner */
+ oldowner = CurrentResourceOwner;
+ CurrentResourceOwner = entry->resowner;
+ oldctx = MemoryContextSwitchTo(entry->context);
+
+ entry->snapname = (char*) palloc0(SNAPSHOT_KEYSIZE);
+ entry->matview_id = matviewOid;
+ entry->before_trig_count = 0;
+ entry->after_trig_count = 0;
+ entry->snapshot = NULL;
+ entry->tables = NIL;
+ entry->has_old = false;
+ entry->has_new = false;
+ entry->reference = 1;
+ entry->pid = MyProcPid;
+
+ entry->snapname[0] = '\0';
+ MemoryContextSwitchTo(oldctx);
+ CurrentResourceOwner = oldowner;
+ }
+
+ entry->before_trig_count++;
+
+ elogif(Debug_print_ivm, INFO, "IVM ivm_immediate_before ref %d, mvid:%d", entry->before_trig_count, matviewOid);
+
+ if (Gp_role == GP_ROLE_DISPATCH && !TRIGGER_FIRED_BY_TRUNCATE(trigdata->tg_event))
+ {
+ snprintf(entry->snapname, SNAPSHOT_KEYSIZE, "%08X-%08X-%d-%d",
+ MyProc->backendId, MyProc->lxid, gp_command_count, entry->before_trig_count);
+ ivm_export_snapshot(matviewOid, entry->snapname);
+ }
+ ivm_set_ts_persitent_name(trigdata, rel->rd_id, matviewOid);
+
+ return PointerGetDatum(NULL);
+}
+
+/*
+ * ivm_immediate_maintenance
+ *
+ * IVM trigger function invoked after base table is modified.
+ * For each table, tuplestores of transition tables are collected.
+ * and after the last modification
+ */
+Datum
+ivm_immediate_maintenance(PG_FUNCTION_ARGS)
+{
+ TriggerData *trigdata = (TriggerData *) fcinfo->context;
+ Relation rel;
+ Oid relid;
+ Oid matviewOid;
+ Query *query;
+ Query *rewritten = NULL;
+ char *matviewOid_text = trigdata->tg_trigger->tgargs[0];
+ Relation matviewRel;
+ int old_depth = matview_maintenance_depth;
+
+ Oid relowner;
+ Tuplestorestate *old_tuplestore = NULL;
+ Tuplestorestate *new_tuplestore = NULL;
+ DestReceiver *dest_new = NULL, *dest_old = NULL;
+ Oid save_userid;
+ int save_sec_context;
+ int save_nestlevel;
+
+ MV_TriggerHashEntry *entry;
+ MV_TriggerTable *table;
+ bool found;
+
+ ParseState *pstate;
+ MemoryContext oldcxt;
+ ListCell *lc;
+ int i;
+ ResourceOwner oldowner;
+
+ QueryEnvironment *queryEnv = create_queryEnv();
+
+ /* Create a ParseState for rewriting the view definition query */
+ pstate = make_parsestate(NULL);
+ pstate->p_queryEnv = queryEnv;
+ pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET;
+
+ rel = trigdata->tg_relation;
+ relid = rel->rd_id;
+
+ matviewOid = DatumGetObjectId(DirectFunctionCall1(oidin, CStringGetDatum(matviewOid_text)));
+
+ /* get the entry for this materialized view */
+ entry = (MV_TriggerHashEntry *) hash_search(mv_trigger_info,
+ (void *) &matviewOid,
+ HASH_FIND, &found);
+ Assert (found && entry != NULL);
+ entry->after_trig_count++;
+
+ elogif(Debug_print_ivm, INFO, "IVM ivm_immediate_maintenance ref %d, mvid:%d", entry->after_trig_count, matviewOid);
+
+ oldowner = CurrentResourceOwner;
+ CurrentResourceOwner = entry->resowner;
+
+ oldcxt = MemoryContextSwitchTo(entry->context);
+ /* search the entry for the modified table and create new entry if not found */
+ found = false;
+ foreach(lc, entry->tables)
+ {
+ table = (MV_TriggerTable *) lfirst(lc);
+ if (table->table_id == relid)
+ {
+ found = true;
+ break;
+ }
+ }
+
+ if (!found)
+ {
+ table = (MV_TriggerTable *) palloc0(sizeof(MV_TriggerTable));
+ table->table_id = relid;
+ table->old_tuplestores = NIL;
+ table->new_tuplestores = NIL;
+ table->rte_indexes = NIL;
+ table->slot = MakeSingleTupleTableSlot(RelationGetDescr(rel), table_slot_callbacks(rel));
+ table->rel = table_open(RelationGetRelid(rel), NoLock);
+ entry->tables = lappend(entry->tables, table);
+ }
+
+ /* Save the transition tables and make a request to not free immediately */
+ if (trigdata->tg_oldtable)
+ {
+ table->old_tuplestores = lappend(table->old_tuplestores, trigdata->tg_oldtable);
+ entry->has_old = true;
+ }
+ if (trigdata->tg_newtable)
+ {
+ table->new_tuplestores = lappend(table->new_tuplestores, trigdata->tg_newtable);
+ entry->has_new = true;
+ }
+ if (entry->has_new || entry->has_old)
+ {
+ CmdType cmd;
+
+ if (TRIGGER_FIRED_BY_INSERT(trigdata->tg_event))
+ cmd = CMD_INSERT;
+ else if (TRIGGER_FIRED_BY_DELETE(trigdata->tg_event))
+ cmd = CMD_DELETE;
+ else if (TRIGGER_FIRED_BY_UPDATE(trigdata->tg_event))
+ cmd = CMD_UPDATE;
+ else
+ elog(ERROR,"unsupported trigger type");
+ /* Prolong lifespan of transition tables to the end of the last AFTER trigger */
+ SetTransitionTablePreserved(relid, cmd);
+ }
+
+
+ /* If this is not the last AFTER trigger call, immediately exit. */
+ Assert (entry->before_trig_count >= entry->after_trig_count);
+ if (entry->before_trig_count != entry->after_trig_count)
+ return PointerGetDatum(NULL);
+
+ /*
+ * If this is the last AFTER trigger call, continue and update the view.
+ */
+
+ /*
+ * Advance command counter to make the updated base table row locally
+ * visible.
+ */
+ CommandCounterIncrement();
+
+ matviewRel = table_open(matviewOid, NoLock);
+
+ /* Make sure it is a materialized view. */
+ Assert(matviewRel->rd_rel->relkind == RELKIND_MATVIEW);
+
+ /*
+ * Get and push the latast snapshot to see any changes which is committed
+ * during waiting in other transactions at READ COMMITTED level.
+ */
+ PushActiveSnapshot(GetTransactionSnapshot());
+
+ /*
+ * 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 a materialized view incrementally");
+
+ /*
+ * 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.
+ * We will switch modes when we are about to execute user code.
+ */
+ relowner = matviewRel->rd_rel->relowner;
+ GetUserIdAndSecContext(&save_userid, &save_sec_context);
+ SetUserIdAndSecContext(relowner,
+ save_sec_context);
+ save_nestlevel = NewGUCNestLevel();
+
+ /* get view query*/
+ query = get_matview_query(matviewRel);
+
+ /*
+ * When a base table is truncated, the view content will be empty if the
+ * view definition query does not contain an outer join or an aggregate
+ * without a GROUP clause. Therefore, such views can be truncated.
+ *
+ * Aggregate views without a GROUP clause always have one row. Therefore,
+ * if a base table is truncated, the view will not be empty and will contain
+ * a row with NULL value (or 0 for count()). So, in this case, we refresh the
+ * view instead of truncating it.
+ */
+ if (TRIGGER_FIRED_BY_TRUNCATE(trigdata->tg_event))
+ {
+ MemoryContextSwitchTo(oldcxt);
+
+ if (!(query->hasAggs && query->groupClause == NIL))
+ ExecuteTruncateGuts(list_make1(matviewRel), list_make1_oid(matviewOid),
+ NIL, DROP_RESTRICT, false, NULL);
+ else if (Gp_role == GP_ROLE_DISPATCH)
+ ExecuteTruncateGuts_IVM(matviewRel, matviewOid, query);
+
+ /* Clean up hash entry and delete tuplestores */
+ clean_up_IVM_hash_entry(entry, false);
+
+ /* Pop the original snapshot. */
+ PopActiveSnapshot();
+
+ 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);
+ /* Restore resource owner */
+ CurrentResourceOwner = oldowner;
+
+ return PointerGetDatum(NULL);
+ }
+
+ /*
+ * rewrite query for calculating deltas
+ */
+
+ rewritten = copyObject(query);
+
+ /* Replace resnames in a target list with materialized view's attnames */
+ i = 0;
+ foreach (lc, rewritten->targetList)
+ {
+ TargetEntry *tle = (TargetEntry *) lfirst(lc);
+ Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, i);
+ char *resname = NameStr(attr->attname);
+
+ tle->resname = pstrdup(resname);
+ i++;
+ }
+ /*
+ * Step1: collect transition tables in QEs and
+ * Set all tables in the query to pre-update state and
+ */
+ rewritten = rewrite_query_for_preupdate_state(rewritten, entry->tables,
+ pstate, matviewOid);
+ /* Rewrite for counting duplicated tuples and aggregates functions*/
+ rewritten = rewrite_query_for_counting_and_aggregates(rewritten, pstate);
+
+ /* Create tuplestores to store view deltas */
+ if (entry->has_old)
+ {
+ MemoryContext cxt = MemoryContextSwitchTo(TopTransactionContext);
+
+ old_tuplestore = tuplestore_begin_heap(false, false, work_mem);
+ dest_old = CreateDestReceiver(DestTuplestore);
+ SetTuplestoreDestReceiverParams(dest_old,
+ old_tuplestore,
+ TopTransactionContext,
+ false,
+ NULL,
+ NULL);
+
+ MemoryContextSwitchTo(cxt);
+ }
+ if (entry->has_new)
+ {
+ MemoryContext cxt = MemoryContextSwitchTo(TopTransactionContext);
+
+ new_tuplestore = tuplestore_begin_heap(false, false, work_mem);
+ dest_new = CreateDestReceiver(DestTuplestore);
+ SetTuplestoreDestReceiverParams(dest_new,
+ new_tuplestore,
+ TopTransactionContext,
+ false,
+ NULL,
+ NULL);
+ MemoryContextSwitchTo(cxt);
+ }
+ /*
+ * Step2: calculate delta tables
+ * Step3: apply delta tables to the materialized view
+ */
+ if (Gp_role == GP_ROLE_DISPATCH)
+ {
+ /* for all modified tables */
+ foreach(lc, entry->tables)
+ {
+ ListCell *lc2;
+
+ table = (MV_TriggerTable *) lfirst(lc);
+
+ /* loop for self-join */
+ foreach(lc2, table->rte_indexes)
+ {
+ int rte_index = lfirst_int(lc2);
+ TupleDesc tupdesc_old;
+ TupleDesc tupdesc_new;
+ Size snaplen = strlen(entry->snapname);
+
+ bool use_count = false;
+ char *count_colname = NULL;
+ char *old_enr = NULL;
+ char *new_enr = NULL;
+
+ count_colname = pstrdup("__ivm_count__");
+ if (query->hasAggs || query->distinctClause)
+ use_count = true;
+
+ configure_queryEnv(queryEnv, matviewOid, table->table_id, entry->snapname, snaplen);
+
+ /* calculate delta tables */
+ old_enr = calc_delta_old(old_tuplestore, matviewRel, table, rte_index, rewritten, dest_old,
+ &tupdesc_old, queryEnv);
+
+ new_enr = calc_delta_new(new_tuplestore, matviewRel, table, rte_index, rewritten, dest_new,
+ &tupdesc_new, queryEnv);
+
+ configure_queryEnv(queryEnv, InvalidOid, InvalidOid, NULL, 0);
+
+ /* Set the table in the query to post-update state */
+ rewritten = rewrite_query_for_postupdate_state(rewritten, table, rte_index);
+
+ PG_TRY();
+ {
+ /* apply the delta tables to the materialized view */
+ apply_delta(old_enr, new_enr, table, matviewOid, old_tuplestore, new_tuplestore,
+ tupdesc_old, tupdesc_new, query, use_count, count_colname);
+ }
+ PG_CATCH();
+ {
+ matview_maintenance_depth = old_depth;
+ PG_RE_THROW();
+ }
+ PG_END_TRY();
+
+ /* clear view delta tuplestores */
+ if (old_tuplestore)
+ tuplestore_clear(old_tuplestore);
+ if (new_tuplestore)
+ tuplestore_clear(new_tuplestore);
+ }
+ }
+ }
+
+ if (old_tuplestore)
+ {
+ dest_old->rDestroy(dest_old);
+ tuplestore_end(old_tuplestore);
+ }
+ if (new_tuplestore)
+ {
+ dest_new->rDestroy(dest_new);
+ tuplestore_end(new_tuplestore);
+ }
+
+ MemoryContextSwitchTo(oldcxt);
+
+ /* Pop the original snapshot. */
+ PopActiveSnapshot();
+
+ 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);
+
+ CurrentResourceOwner = oldowner;
+ /*
+ * Step4: cleanup stage
+ */
+ if (Gp_role == GP_ROLE_DISPATCH)
+ {
+ apply_cleanup(matviewOid);
+ DirectFunctionCall1(ivm_immediate_cleanup, ObjectIdGetDatum(matviewOid));
+ }
+ return PointerGetDatum(NULL);
+}
+
+/*
+ * rewrite_query_for_preupdate_state
+ *
+ * Rewrite the query so that base tables' RTEs will represent "pre-update"
+ * state of tables. This is necessary to calculate view delta after multiple
+ * tables are modified.
+ */
+static Query*
+rewrite_query_for_preupdate_state(Query *query, List *tables,
+ ParseState *pstate, Oid matviewid)
+{
+ ListCell *lc;
+ int num_rte = list_length(query->rtable);
+ int i;
+
+
+ /* register delta ENRs */
+ register_delta_ENRs(pstate, query, tables);
+
+ /* XXX: Is necessary? Is this right timing? */
+ AcquireRewriteLocks(query, true, false);
+
+ i = 1;
+
+ foreach(lc, query->rtable)
+ {
+ RangeTblEntry *r = (RangeTblEntry*) lfirst(lc);
+
+ ListCell *lc2;
+ foreach(lc2, tables)
+ {
+ MV_TriggerTable *table = (MV_TriggerTable *) lfirst(lc2);
+ /*
+ * if the modified table is found then replace the original RTE with
+ * "pre-state" RTE and append its index to the list.
+ */
+ if (r->relid == table->table_id)
+ {
+ List *securityQuals;
+ List *withCheckOptions;
+ bool hasRowSecurity;
+ bool hasSubLinks;
+
+ RangeTblEntry *rte_pre = get_prestate_rte(r, table, pstate->p_queryEnv, matviewid);
+ /*
+ * Set a row security poslicies of the modified table to the subquery RTE which
+ * represents the pre-update state of the table.
+ */
+ get_row_security_policies(query, table->original_rte, i,
+ &securityQuals, &withCheckOptions,
+ &hasRowSecurity, &hasSubLinks);
+
+ if (hasRowSecurity)
+ {
+ query->hasRowSecurity = true;
+ rte_pre->security_barrier = true;
+ }
+ if (hasSubLinks)
+ query->hasSubLinks = true;
+
+ rte_pre->securityQuals = securityQuals;
+ lfirst(lc) = rte_pre;
+
+ table->rte_indexes = list_append_unique_int(table->rte_indexes, i);
+ break;
+ }
+ }
+
+ /* finish the loop if we processed all RTE included in the original query */
+ if (i++ >= num_rte)
+ break;
+ }
+
+ return query;
+}
+
+/*
+ * register_delta_ENRs
+ *
+ * For all modified tables, make ENRs for their transition tables
+ * and register them to the queryEnv. ENR's RTEs are also appended
+ * into the list in query tree.
+ */
+static void
+register_delta_ENRs(ParseState *pstate, Query *query, List *tables)
+{
+ QueryEnvironment *queryEnv = pstate->p_queryEnv;
+ ListCell *lc;
+ RangeTblEntry *rte;
+
+ foreach(lc, tables)
+ {
+ MV_TriggerTable *table = (MV_TriggerTable *) lfirst(lc);
+ ListCell *lc2;
+ int count;
+
+ count = 0;
+ foreach(lc2, table->old_tuplestores)
+ {
+ Tuplestorestate *oldtable = (Tuplestorestate *) lfirst(lc2);
+ bool freezed = tuplestore_in_freezed(oldtable);
+ EphemeralNamedRelation enr =
+ palloc(sizeof(EphemeralNamedRelationData));
+ ParseNamespaceItem *nsitem;
+ char* shared_name = tuplestore_get_sharedname(oldtable);
+
+ if (freezed || shared_name)
+ enr->md.name = pstrdup(shared_name);
+ else
+ enr->md.name = make_delta_enr_name("old", table->table_id, gp_command_count);
+ enr->md.reliddesc = table->table_id;
+ enr->md.tupdesc = CreateTupleDescCopy(table->rel->rd_att);
+ enr->md.enrtype = ENR_NAMED_TUPLESTORE;
+ enr->md.enrtuples = tuplestore_tuple_count(oldtable);
+ enr->reldata = NULL;
+ register_ENR(queryEnv, enr);
+
+ nsitem = addRangeTableEntryForENR(pstate, makeRangeVar(NULL, enr->md.name, -1), true);
+ rte = nsitem->p_rte;
+ query->rtable = list_append_unique_ptr(query->rtable, rte);
+
+ count++;
+ /* Note: already freezed case */
+ if (freezed)
+ {
+ continue;
+ }
+ tuplestore_make_sharedV2(oldtable,
+ get_shareinput_fileset(),
+ enr->md.name,
+ tuplestore_get_resowner(oldtable));
+ tuplestore_freeze(oldtable);
+ }
+
+ count = 0;
+ foreach(lc2, table->new_tuplestores)
+ {
+ Tuplestorestate *newtable = (Tuplestorestate *) lfirst(lc2);
+ bool freezed = tuplestore_in_freezed(newtable);
+ EphemeralNamedRelation enr =
+ palloc(sizeof(EphemeralNamedRelationData));
+ ParseNamespaceItem *nsitem;
+ char* shared_name = tuplestore_get_sharedname(newtable);
+
+ if (freezed || shared_name)
+ enr->md.name = pstrdup(shared_name);
+ else
+ enr->md.name = make_delta_enr_name("new", table->table_id, gp_command_count);
+ enr->md.reliddesc = table->table_id;
+ enr->md.tupdesc = CreateTupleDescCopy(table->rel->rd_att);
+ enr->md.enrtype = ENR_NAMED_TUPLESTORE;
+ enr->md.enrtuples = tuplestore_tuple_count(newtable);
+ enr->reldata = NULL;
+ register_ENR(queryEnv, enr);
+
+ nsitem = addRangeTableEntryForENR(pstate, makeRangeVar(NULL, enr->md.name, -1), true);
+ rte = nsitem->p_rte;
+
+ query->rtable = list_append_unique_ptr(query->rtable, rte);
+
+ count++;
+ /* Note: already freezed case */
+ if (freezed)
+ {
+ continue;
+ }
+ tuplestore_make_sharedV2(newtable,
+ get_shareinput_fileset(),
+ enr->md.name,
+ tuplestore_get_resowner(newtable));
+ tuplestore_freeze(newtable);
+ }
+ }
+}
+
+#define DatumGetItemPointer(X) ((ItemPointer) DatumGetPointer(X))
+#define PG_GETARG_ITEMPOINTER(n) DatumGetItemPointer(PG_GETARG_DATUM(n))
+
+/*
+ * ivm_visible_in_prestate
+ *
+ * Check visibility of a tuple specified by the tableoid and item pointer
+ * using the snapshot taken just before the table was modified.
+ */
+Datum
+ivm_visible_in_prestate(PG_FUNCTION_ARGS)
+{
+ Oid tableoid = PG_GETARG_OID(0);
+ ItemPointer itemPtr = PG_GETARG_ITEMPOINTER(1);
+ Oid matviewOid = PG_GETARG_OID(2);
+ ListCell *lc;
+ bool result = true;
+ bool found = false;
+ MemoryContext oldcxt;
+ ResourceOwner oldowner;
+
+ MV_TriggerHashEntry *entry;
+ MV_TriggerTable *table = NULL;
+
+ entry = (MV_TriggerHashEntry *) hash_search(mv_trigger_info,
+ (void *) &matviewOid,
+ HASH_FIND, &found);
+ Assert (found && entry != NULL);
+
+ foreach(lc, entry->tables)
+ {
+ table = (MV_TriggerTable *) lfirst(lc);
+ if (table->table_id == tableoid)
+ break;
+ }
+
+ Assert (table != NULL);
+ if (table->rel == NULL && table->slot == NULL)
+ {
+ oldowner = CurrentResourceOwner;
+ CurrentResourceOwner = entry->resowner;
+ oldcxt = MemoryContextSwitchTo(entry->context);
+ /* Lazy open relation */
+ table->rel = table_open(tableoid, NoLock);
+ table->slot = MakeSingleTupleTableSlot(RelationGetDescr(table->rel), table_slot_callbacks(table->rel));
+
+ MemoryContextSwitchTo(oldcxt);
+ CurrentResourceOwner = oldowner;
+ }
+
+ result = table_tuple_fetch_row_version(table->rel, itemPtr, entry->snapshot, table->slot);
+
+ ExecClearTuple(table->slot);
+
+ elogif(Debug_print_ivm, INFO, "IVM tableoid: %d, ctid: %s, visible %d.", tableoid, ItemPointerToString(itemPtr), result);
+ PG_RETURN_BOOL(result);
+}
+
+/*
+ * get_prestate_rte
+ *
+ * Rewrite RTE of the modified table to a subquery which represents
+ * "pre-state" table. The original RTE is saved in table->rte_original.
+ */
+static RangeTblEntry*
+get_prestate_rte(RangeTblEntry *rte, MV_TriggerTable *table,
+ QueryEnvironment *queryEnv, Oid matviewid)
+{
+ StringInfoData str;
+ RawStmt *raw;
+ Query *subquery;
+ Relation rel;
+ ParseState *pstate;
+ char *relname;
+ ListCell *lc;
+
+ pstate = make_parsestate(NULL);
+ pstate->p_queryEnv = queryEnv;
+ pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET;
+
+ /*
+ * We can use NoLock here since AcquireRewriteLocks should
+ * have locked the relation already.
+ */
+ rel = table_open(table->table_id, NoLock);
+ relname = quote_qualified_identifier(
+ get_namespace_name(RelationGetNamespace(rel)),
+ RelationGetRelationName(rel));
+ table_close(rel, NoLock);
+
+ /*
+ * Filtering inserted row using the snapshot taken before the table
+ * is modified. is required for maintaining outer join views.
+ */
+ initStringInfo(&str);
+ appendStringInfo(&str,
+ "SELECT t.* FROM %s t"
+ " WHERE pg_catalog.ivm_visible_in_prestate(t.tableoid, t.ctid,%d::pg_catalog.oid, t.gp_segment_id)",
+ relname, matviewid);
+
+ /*
+ * Append deleted rows contained in old transition tables.
+ */
+ foreach(lc, table->old_tuplestores)
+ {
+ Tuplestorestate *tuplestore = (Tuplestorestate *) lfirst(lc);
+ appendStringInfo(&str, " UNION ALL ");
+ appendStringInfo(&str," SELECT * FROM %s",
+ tuplestore_get_sharedname(tuplestore));
+ }
+
+ /* Get a subquery representing pre-state of the table */
+ raw = (RawStmt*)linitial(raw_parser(str.data, RAW_PARSE_DEFAULT));
+ subquery = transformStmt(pstate, raw->stmt);
+
+ /* save the original RTE */
+ table->original_rte = copyObject(rte);
+
+ rte->rtekind = RTE_SUBQUERY;
+ rte->subquery = subquery;
+ rte->security_barrier = false;
+
+ /* 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 */
+
+ return rte;
+}
+
+/*
+ * make_delta_enr_name
+ *
+ * Make a name for ENR of a transition table from the base table's oid.
+ * prefix will be "new" or "old" depending on its transition table kind..
+ */
+static char*
+make_delta_enr_name(const char *prefix, Oid relid, int count)
+{
+ char buf[NAMEDATALEN];
+ char *name;
+
+ snprintf(buf, NAMEDATALEN, "__ivm_%s_%u_%u", prefix, relid, count);
+ name = pstrdup(buf);
+
+ return name;
+}
+
+/*
+ * replace_rte_with_delta
+ *
+ * Replace RTE of the modified table with a single table delta that combine its
+ * all transition tables.
+ */
+static RangeTblEntry*
+replace_rte_with_delta(RangeTblEntry *rte, MV_TriggerTable *table, bool is_new,
+ QueryEnvironment *queryEnv)
+{
+ StringInfoData str;
+ ParseState *pstate;
+ RawStmt *raw;
+ Query *sub;
+ int i;
+ ListCell *lc;
+ List *ts = is_new ? table->new_tuplestores : table->old_tuplestores;
+
+ /* the previous RTE must be a subquery which represents "pre-state" table */
+ Assert(rte->rtekind == RTE_SUBQUERY);
+
+ /* Create a ParseState for rewriting the view definition query */
+ pstate = make_parsestate(NULL);
+ pstate->p_queryEnv = queryEnv;
+ pstate->p_expr_kind = EXPR_KIND_SELECT_TARGET;
+
+ initStringInfo(&str);
+
+ i = 0;
+ foreach(lc, ts)
+ {
+ Tuplestorestate *tuplestore = (Tuplestorestate *) lfirst(lc);
+ if (i > 0)
+ appendStringInfo(&str, " UNION ALL ");
+ appendStringInfo(&str,
+ " SELECT * FROM %s",
+ tuplestore_get_sharedname(tuplestore));
+ i++;
+ }
+
+ raw = (RawStmt*)linitial(raw_parser(str.data, RAW_PARSE_DEFAULT));
+ sub = transformStmt(pstate, raw->stmt);
+
+ /*
+ * Update the subquery so that it represent the combined transition
+ * table. Note that we leave the security_barrier and securityQuals
+ * fields so that the subquery relation can be protected by the RLS
+ * policy as same as the modified table.
+ */
+ rte->rtekind = RTE_SUBQUERY;
+ rte->subquery = sub;
+ rte->security_barrier = false;
+
+ /* 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 */
+
+ return rte;
+}
+
+/*
+ * rewrite_query_for_counting_and_aggregates
+ *
+ * Rewrite query for counting duplicated tuples and aggregate functions.
+ */
+static Query *
+rewrite_query_for_counting_and_aggregates(Query *query, ParseState *pstate)
+{
+ TargetEntry *tle_count;
+ FuncCall *fn;
+ Node *node;
+
+ /* For aggregate views */
+ if (query->hasAggs)
+ {
+ ListCell *lc;
+ List *aggs = NIL;
+ AttrNumber next_resno = list_length(query->targetList) + 1;
+
+ foreach(lc, query->targetList)
+ {
+ TargetEntry *tle = (TargetEntry *) lfirst(lc);
+
+ if (IsA(tle->expr, Aggref))
+ makeIvmAggColumn(pstate, (Aggref *)tle->expr, tle->resname, &next_resno, &aggs);
+ }
+ query->targetList = list_concat(query->targetList, aggs);
+ }
+ /* Add count(*) for counting distinct tuples in views */
+ fn = makeFuncCall(SystemFuncName("count"), NIL, COERCE_EXPLICIT_CALL, -1);
+ fn->agg_star = true;
+ if (!query->groupClause && !query->hasAggs)
+ query->groupClause = transformDistinctClause(NULL, &query->targetList, query->sortClause, false);
+
+ node = ParseFuncOrColumn(pstate, fn->funcname, NIL, NULL, fn, false, -1);
+
+ tle_count = makeTargetEntry((Expr *) node,
+ list_length(query->targetList) + 1,
+ pstrdup("__ivm_count__"),
+ false);
+ query->targetList = lappend(query->targetList, tle_count);
+ query->hasAggs = true;
+
+ return query;
+}
+
+/*
+ * calc_delta
+ *
+ * Calculate view deltas generated under the modification of a table specified
+ * by the RTE index.
+ */
+static char*
+calc_delta_old(Tuplestorestate *ts, Relation matviewRel, MV_TriggerTable *table, int rte_index, Query *query,
+ DestReceiver *dest_old,
+ TupleDesc *tupdesc_old,
+ QueryEnvironment *queryEnv)
+{
+ ListCell *lc = list_nth_cell(query->rtable, rte_index - 1);
+ RangeTblEntry *rte = (RangeTblEntry *) lfirst(lc);
+ uint64 es_processed = 0;
+ char *enrname = NULL;
+
+ /* Generate old delta */
+ if (list_length(table->old_tuplestores) > 0)
+ {
+ /* Replace the modified table with the old delta table and calculate the old view delta. */
+ replace_rte_with_delta(rte, table, false, queryEnv);
+ enrname = make_delta_enr_name(OLD_DELTA_ENRNAME, RelationGetRelid(matviewRel), gp_command_count);
+ es_processed = refresh_matview_memoryfill(dest_old, query, queryEnv,
+ tupdesc_old, enrname, matviewRel);
+ if (ts)
+ tuplestore_set_tuplecount(ts, es_processed);
+ }
+
+ return enrname;
+}
+
+static char*
+calc_delta_new(Tuplestorestate *ts, Relation matviewRel, MV_TriggerTable *table, int rte_index, Query *query,
+ DestReceiver *dest_new,
+ TupleDesc *tupdesc_new,
+ QueryEnvironment *queryEnv)
+{
+ ListCell *lc = list_nth_cell(query->rtable, rte_index - 1);
+ RangeTblEntry *rte = (RangeTblEntry *) lfirst(lc);
+ uint64 es_processed = 0;
+ char *enrname = NULL;
+
+ /* Generate new delta */
+ if (list_length(table->new_tuplestores) > 0)
+ {
+ /* Replace the modified table with the new delta table and calculate the new view delta*/
+ replace_rte_with_delta(rte, table, true, queryEnv);
+ enrname = make_delta_enr_name(NEW_DELTA_ENRNAME, RelationGetRelid(matviewRel), gp_command_count);
+ es_processed = refresh_matview_memoryfill(dest_new, query, queryEnv,
+ tupdesc_new, enrname, matviewRel);
+ if (ts)
+ tuplestore_set_tuplecount(ts, es_processed);
+ }
+
+ return enrname;
+}
+
+/*
+ * rewrite_query_for_postupdate_state
+ *
+ * Rewrite the query so that the specified base table's RTEs will represent
+ * "post-update" state of tables. This is called after the view delta
+ * calculation due to changes on this table finishes.
+ */
+static Query*
+rewrite_query_for_postupdate_state(Query *query, MV_TriggerTable *table, int rte_index)
+{
+ ListCell *lc = list_nth_cell(query->rtable, rte_index - 1);
+
+ /* Retore the original RTE */
+ lfirst(lc) = table->original_rte;
+
+ return query;
+}
+
+#define IVM_colname(type, col) makeObjectName("__ivm_" type, col, "_")
+
+/*
+ * apply_delta
+ *
+ * Apply deltas to the materialized view. In outer join cases, this requires
+ * the view maintenance graph.
+ */
+static void
+apply_delta(char *old_enr, char *new_enr, MV_TriggerTable *table, Oid matviewOid,
+ Tuplestorestate *old_tuplestores, Tuplestorestate *new_tuplestores,
+ TupleDesc tupdesc_old, TupleDesc tupdesc_new,
+ Query *query, bool use_count, char *count_colname)
+{
+ StringInfoData querybuf;
+ StringInfoData target_list_buf;
+ StringInfo aggs_list_buf = NULL;
+ StringInfo aggs_set_old = NULL;
+ StringInfo aggs_set_new = NULL;
+ Relation matviewRel;
+ char *matviewname;
+ ListCell *lc;
+ int i;
+ List *keys = NIL;
+
+
+ /*
+ * get names of the materialized view and delta tables
+ */
+
+ matviewRel = table_open(matviewOid, NoLock);
+ matviewname = quote_qualified_identifier(get_namespace_name(RelationGetNamespace(matviewRel)),
+ RelationGetRelationName(matviewRel));
+
+ /*
+ * Build parts of the maintenance queries
+ */
+
+ initStringInfo(&querybuf);
+ initStringInfo(&target_list_buf);
+
+ if (query->hasAggs)
+ {
+ if (old_tuplestores && tuplestore_tuple_count(old_tuplestores) > 0)
+ aggs_set_old = makeStringInfo();
+ if (new_tuplestores && tuplestore_tuple_count(new_tuplestores) > 0)
+ aggs_set_new = makeStringInfo();
+ aggs_list_buf = makeStringInfo();
+ }
+
+ /* build string of target list */
+ for (i = 0; i < matviewRel->rd_att->natts; i++)
+ {
+ Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, i);
+ char *resname = NameStr(attr->attname);
+ if (i != 0)
+ appendStringInfo(&target_list_buf, ", ");
+ appendStringInfo(&target_list_buf, "%s", quote_qualified_identifier(NULL, resname));
+ }
+
+ i = 0;
+ foreach (lc, query->targetList)
+ {
+ TargetEntry *tle = (TargetEntry *) lfirst(lc);
+ Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, i);
+ char *resname = NameStr(attr->attname);
+
+ i++;
+ if (tle->resjunk)
+ continue;
+
+ /*
+ * For views without aggregates, all attributes are used as keys to identify a
+ * tuple in a view.
+ */
+ if (!query->hasAggs)
+ keys = lappend(keys, attr);
+
+ /* For views with aggregates, we need to build SET clause for updating aggregate
+ * values. */
+ if (query->hasAggs && IsA(tle->expr, Aggref))
+ {
+ Aggref *aggref = (Aggref *) tle->expr;
+ const char *aggname = get_func_name(aggref->aggfnoid);
+
+ /*
+ * We can use function names here because it is already checked if these
+ * can be used in IMMV by its OID at the definition time.
+ */
+
+ /* count */
+ if (!strcmp(aggname, "count"))
+ append_set_clause_for_count(resname, aggs_set_old, aggs_set_new, aggs_list_buf);
+
+ /* sum */
+ else if (!strcmp(aggname, "sum"))
+ append_set_clause_for_sum(resname, aggs_set_old, aggs_set_new, aggs_list_buf);
+
+ /* avg */
+ else if (!strcmp(aggname, "avg"))
+ append_set_clause_for_avg(resname, aggs_set_old, aggs_set_new, aggs_list_buf,
+ format_type_be(aggref->aggtype));
+ else
+ elog(ERROR, "unsupported aggregate function: %s", aggname);
+ }
+ }
+ /* If we have GROUP BY clause, we use its entries as keys. */
+ if (query->hasAggs && query->groupClause)
+ {
+ foreach (lc, query->groupClause)
+ {
+ SortGroupClause *sgcl = (SortGroupClause *) lfirst(lc);
+ TargetEntry *tle = get_sortgroupclause_tle(sgcl, query->targetList);
+ Form_pg_attribute attr = TupleDescAttr(matviewRel->rd_att, tle->resno - 1);
+
+ keys = lappend(keys, attr);
+ }
+ }
+ /* Start maintaining the materialized view. */
+ OpenMatViewIncrementalMaintenance();
+
+ /* Open SPI context. */
+ if (SPI_connect() != SPI_OK_CONNECT)
+ elog(ERROR, "SPI_connect failed");
+
+ /* For tuple deletion */
+ if (old_tuplestores && tuplestore_tuple_count(old_tuplestores) > 0)
+ {
+ int rc;
+ EphemeralNamedRelation enr = palloc(sizeof(EphemeralNamedRelationData));
+ /* convert tuplestores to ENR, and register for SPI */
+ enr->md.name = pstrdup(old_enr);
+ enr->md.reliddesc = matviewOid;
+ enr->md.tupdesc = CreateTupleDescCopy(tupdesc_old);
+ enr->md.enrtype = ENR_NAMED_TUPLESTORE;
+ enr->md.enrtuples = tuplestore_tuple_count(old_tuplestores);
+ enr->reldata = NULL;
+
+ rc = SPI_register_relation(enr);
+ if (rc != SPI_OK_REL_REGISTER)
+ elog(ERROR, "SPI_register failed");
+
+ elogif(Debug_print_ivm, INFO, "IVM apply old enr %s, command_count: %d", enr->md.name, gp_command_count);
+ if (use_count)
+ /* apply old delta and get rows to be recalculated */
+ apply_old_delta_with_count(matviewname, enr->md.name,
+ keys, aggs_list_buf, aggs_set_old,
+ count_colname);
+ else
+ apply_old_delta(matviewname, enr->md.name, keys);
+ }
+ /* For tuple insertion */
+ if (new_tuplestores && tuplestore_tuple_count(new_tuplestores) > 0)
+ {
+ int rc;
+ EphemeralNamedRelation enr = palloc(sizeof(EphemeralNamedRelationData));
+
+ /* convert tuplestores to ENR, and register for SPI */
+ enr->md.name = pstrdup(new_enr);
+ enr->md.reliddesc = matviewOid;
+ enr->md.tupdesc = CreateTupleDescCopy(tupdesc_new);
+ enr->md.enrtype = ENR_NAMED_TUPLESTORE;
+ enr->md.enrtuples = tuplestore_tuple_count(new_tuplestores);
+ enr->reldata = NULL;
+
+ rc = SPI_register_relation(enr);
+ if (rc != SPI_OK_REL_REGISTER)
+ elog(ERROR, "SPI_register failed");
+
+ elogif(Debug_print_ivm, INFO, "IVM apply new enr %s, command_count: %d", enr->md.name, gp_command_count);
+ /* apply new delta */
+ if (use_count)
+ apply_new_delta_with_count(matviewname, enr->md.name,
+ keys, aggs_set_new,
+ &target_list_buf, count_colname);
+ else
+ apply_new_delta(matviewname, enr->md.name, &target_list_buf);
+ }
+
+ /* We're done maintaining the materialized view. */
+ CloseMatViewIncrementalMaintenance();
+
+ table_close(matviewRel, NoLock);
+
+ /* Close SPI context. */
+ if (SPI_finish() != SPI_OK_FINISH)
+ elog(ERROR, "SPI_finish failed");
+}
+
+/*
+ * append_set_clause_for_count
+ *
+ * Append SET clause string for count aggregation to given buffers.
+ * Also, append resnames required for calculating the aggregate value.
+ */
+static void
+append_set_clause_for_count(const char *resname, StringInfo buf_old,
+ StringInfo buf_new,StringInfo aggs_list)
+{
+ /* For tuple deletion */
+ if (buf_old)
+ {
+ /* resname = mv.resname - t.resname */
+ appendStringInfo(buf_old,
+ ", %s = %s",
+ quote_qualified_identifier(NULL, resname),
+ get_operation_string(IVM_SUB, resname, "mv", "t", NULL, NULL));
+ }
+ /* For tuple insertion */
+ if (buf_new)
+ {
+ /* resname = mv.resname + diff.resname */
+ appendStringInfo(buf_new,
+ ", %s = %s",
+ quote_qualified_identifier(NULL, resname),
+ get_operation_string(IVM_ADD, resname, "mv", "diff", NULL, NULL));
+ }
+
+ appendStringInfo(aggs_list, ", %s",
+ quote_qualified_identifier("diff", resname)
+ );
+}
+
+/*
+ * append_set_clause_for_sum
+ *
+ * Append SET clause string for sum aggregation to given buffers.
+ * Also, append resnames required for calculating the aggregate value.
+ */
+static void
+append_set_clause_for_sum(const char *resname, StringInfo buf_old,
+ StringInfo buf_new, StringInfo aggs_list)
+{
+ char *count_col = IVM_colname("count", resname);
+
+ /* For tuple deletion */
+ if (buf_old)
+ {
+ /* sum = mv.sum - t.sum */
+ appendStringInfo(buf_old,
+ ", %s = %s",
+ quote_qualified_identifier(NULL, resname),
+ get_operation_string(IVM_SUB, resname, "mv", "t", count_col, NULL)
+ );
+ /* count = mv.count - t.count */
+ appendStringInfo(buf_old,
+ ", %s = %s",
+ quote_qualified_identifier(NULL, count_col),
+ get_operation_string(IVM_SUB, count_col, "mv", "t", NULL, NULL)
+ );
+ }
+ /* For tuple insertion */
+ if (buf_new)
+ {
+ /* sum = mv.sum + diff.sum */
+ appendStringInfo(buf_new,
+ ", %s = %s",
+ quote_qualified_identifier(NULL, resname),
+ get_operation_string(IVM_ADD, resname, "mv", "diff", count_col, NULL)
+ );
+ /* count = mv.count + diff.count */
+ appendStringInfo(buf_new,
+ ", %s = %s",
+ quote_qualified_identifier(NULL, count_col),
+ get_operation_string(IVM_ADD, count_col, "mv", "diff", NULL, NULL)
+ );
+ }
+
+ appendStringInfo(aggs_list, ", %s, %s",
+ quote_qualified_identifier("diff", resname),
+ quote_qualified_identifier("diff", IVM_colname("count", resname))
+ );
+}
+
+/*
+ * append_set_clause_for_avg
+ *
+ * Append SET clause string for avg aggregation to given buffers.
+ * Also, append resnames required for calculating the aggregate value.
+ */
+static void
+append_set_clause_for_avg(const char *resname, StringInfo buf_old,
+ StringInfo buf_new, StringInfo aggs_list,
+ const char *aggtype)
+{
+ char *sum_col = IVM_colname("sum", resname);
+ char *count_col = IVM_colname("count", resname);
+
+ /* For tuple deletion */
+ if (buf_old)
+ {
+ /* avg = (mv.sum - t.sum)::aggtype / (mv.count - t.count) */
+ appendStringInfo(buf_old,
+ ", %s = %s OPERATOR(pg_catalog./) %s",
+ quote_qualified_identifier(NULL, resname),
+ get_operation_string(IVM_SUB, sum_col, "mv", "t", count_col, aggtype),
+ get_operation_string(IVM_SUB, count_col, "mv", "t", NULL, NULL)
+ );
+ /* sum = mv.sum - t.sum */
+ appendStringInfo(buf_old,
+ ", %s = %s",
+ quote_qualified_identifier(NULL, sum_col),
+ get_operation_string(IVM_SUB, sum_col, "mv", "t", count_col, NULL)
+ );
+ /* count = mv.count - t.count */
+ appendStringInfo(buf_old,
+ ", %s = %s",
+ quote_qualified_identifier(NULL, count_col),
+ get_operation_string(IVM_SUB, count_col, "mv", "t", NULL, NULL)
+ );
+
+ }
+ /* For tuple insertion */
+ if (buf_new)
+ {
+ /* avg = (mv.sum + diff.sum)::aggtype / (mv.count + diff.count) */
+ appendStringInfo(buf_new,
+ ", %s = %s OPERATOR(pg_catalog./) %s",
+ quote_qualified_identifier(NULL, resname),
+ get_operation_string(IVM_ADD, sum_col, "mv", "diff", count_col, aggtype),
+ get_operation_string(IVM_ADD, count_col, "mv", "diff", NULL, NULL)
+ );
+ /* sum = mv.sum + diff.sum */
+ appendStringInfo(buf_new,
+ ", %s = %s",
+ quote_qualified_identifier(NULL, sum_col),
+ get_operation_string(IVM_ADD, sum_col, "mv", "diff", count_col, NULL)
+ );
+ /* count = mv.count + diff.count */
+ appendStringInfo(buf_new,
+ ", %s = %s",
+ quote_qualified_identifier(NULL, count_col),
+ get_operation_string(IVM_ADD, count_col, "mv", "diff", NULL, NULL)
+ );
+ }
+
+ appendStringInfo(aggs_list, ", %s, %s, %s",
+ quote_qualified_identifier("diff", resname),
+ quote_qualified_identifier("diff", IVM_colname("sum", resname)),
+ quote_qualified_identifier("diff", IVM_colname("count", resname))
+ );
+}
+
+/*
+ * get_operation_string
+ *
+ * Build a string to calculate the new aggregate values.
+ */
+static char *
+get_operation_string(IvmOp op, const char *col, const char *arg1, const char *arg2,
+ const char* count_col, const char *castType)
+{
+ StringInfoData buf;
+ StringInfoData castString;
+ char *col1 = quote_qualified_identifier(arg1, col);
+ char *col2 = quote_qualified_identifier(arg2, col);
+ char op_char = (op == IVM_SUB ? '-' : '+');
+
+ initStringInfo(&buf);
+ initStringInfo(&castString);
+
+ if (castType)
+ appendStringInfo(&castString, "::%s", castType);
+
+ if (!count_col)
+ {
+ /*
+ * If the attributes don't have count columns then calc the result
+ * by using the operator simply.
+ */
+ appendStringInfo(&buf, "(%s OPERATOR(pg_catalog.%c) %s)%s",
+ col1, op_char, col2, castString.data);
+ }
+ else
+ {
+ /*
+ * If the attributes have count columns then consider the condition
+ * where the result becomes NULL.
+ */
+ char *null_cond = get_null_condition_string(op, arg1, arg2, count_col);
+
+ appendStringInfo(&buf,
+ "(CASE WHEN %s THEN NULL "
+ "WHEN %s IS NULL THEN %s "
+ "WHEN %s IS NULL THEN %s "
+ "ELSE (%s OPERATOR(pg_catalog.%c) %s)%s END)",
+ null_cond,
+ col1, col2,
+ col2, col1,
+ col1, op_char, col2, castString.data
+ );
+ }
+
+ return buf.data;
+}
+
+/*
+ * get_null_condition_string
+ *
+ * Build a predicate string for CASE clause to check if an aggregate value
+ * will became NULL after the given operation is applied.
+ */
+static char *
+get_null_condition_string(IvmOp op, const char *arg1, const char *arg2,
+ const char* count_col)
+{
+ StringInfoData null_cond;
+ initStringInfo(&null_cond);
+
+ switch (op)
+ {
+ case IVM_ADD:
+ appendStringInfo(&null_cond,
+ "%s OPERATOR(pg_catalog.=) 0 AND %s OPERATOR(pg_catalog.=) 0",
+ quote_qualified_identifier(arg1, count_col),
+ quote_qualified_identifier(arg2, count_col)
+ );
+ break;
+ case IVM_SUB:
+ appendStringInfo(&null_cond,
+ "%s OPERATOR(pg_catalog.=) %s",
+ quote_qualified_identifier(arg1, count_col),
+ quote_qualified_identifier(arg2, count_col)
+ );
+ break;
+ default:
+ elog(ERROR,"unknown operation");
+ }
+
+ return null_cond.data;
+}
+
+
+/*
+ * apply_old_delta_with_count
+ *
+ * Execute a query for applying a delta table given by deltname_old
+ * which contains tuples to be deleted from to a materialized view given by
+ * matviewname. This is used when counting is required, that is, the view
+ * has aggregate or distinct.
+ *
+ * If the view desn't have aggregates or has GROUP BY, this requires a keys
+ * list to identify a tuple in the view. If the view has aggregates, this
+ * requires strings representing resnames of aggregates and SET clause for
+ * updating aggregate values.
+ */
+static void
+apply_old_delta_with_count(const char *matviewname, const char *deltaname_old,
+ List *keys, StringInfo aggs_list, StringInfo aggs_set,
+ const char *count_colname)
+{
+ StringInfoData querybuf;
+ StringInfoData tselect;
+ char *match_cond;
+ bool agg_without_groupby = (list_length(keys) == 0);
+
+ /* build WHERE condition for searching tuples to be deleted */
+ match_cond = get_matching_condition_string(keys);
+
+#if 0
+ initStringInfo(&querybuf);
+ appendStringInfo(&querybuf,
+ "WITH t AS (" /* collecting tid of target tuples in the view */
+ "SELECT diff.%s, " /* count column */
+ "(diff.%s OPERATOR(pg_catalog.=) mv.%s AND %s) AS for_dlt, "
+ "mv.ctid "
+ "%s " /* aggregate columns */
+ "FROM %s AS mv, %s AS diff "
+ "WHERE %s" /* tuple matching condition */
+ "), updt AS (" /* update a tuple if this is not to be deleted */
+ "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s "
+ "%s" /* SET clauses for aggregates */
+ "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND NOT for_dlt "
+ ")"
+ /* delete a tuple if this is to be deleted */
+ "DELETE FROM %s AS mv USING t "
+ "WHERE mv.ctid OPERATOR(pg_catalog.=) t.ctid AND for_dlt",
+ count_colname,
+ count_colname, count_colname, (agg_without_groupby ? "false" : "true"),
+ (aggs_list != NULL ? aggs_list->data : ""),
+ matviewname, deltaname_old,
+ match_cond,
+ matviewname, count_colname, count_colname, count_colname,
+ (aggs_set != NULL ? aggs_set->data : ""),
+ matviewname);
+#else
+ /* CBDB_IVM_FIXME: use tuplestore to replace temp table. */
+ initStringInfo(&tselect);
+ initStringInfo(&querybuf);
+ appendStringInfo(&tselect,
+ "CREATE TEMP TABLE t AS SELECT diff.%s, " /* count column */
+ "(diff.%s OPERATOR(pg_catalog.=) mv.%s AND %s) AS for_dlt, "
+ "mv.ctid AS tid, mv.gp_segment_id AS gid"
+ "%s " /* aggregate columns */
+ "FROM %s AS mv, %s AS diff "
+ "WHERE %s DISTRIBUTED RANDOMLY", /* tuple matching condition */
+ count_colname,
+ count_colname, count_colname, (agg_without_groupby ? "false" : "true"),
+ (aggs_list != NULL ? aggs_list->data : ""),
+ matviewname, deltaname_old,
+ match_cond);
+
+ /* Create the temporary table. */
+ if (SPI_exec(tselect.data, 0) != SPI_OK_UTILITY)
+ elog(ERROR, "SPI_exec failed: %s", tselect.data);
+
+ /* Search for matching tuples from the view and update or delete if found. */
+ appendStringInfo(&querybuf,
+ "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.-) t.%s "
+ "%s" /* SET clauses for aggregates */
+ "FROM t WHERE mv.ctid OPERATOR(pg_catalog.=) t.tid"
+ " AND mv.gp_segment_id OPERATOR(pg_catalog.=) t.gid"
+ " AND NOT for_dlt ",
+ matviewname, count_colname, count_colname, count_colname,
+ (aggs_set != NULL ? aggs_set->data : ""));
+ if (SPI_exec(querybuf.data, 0) != SPI_OK_UPDATE)
+ elog(ERROR, "SPI_exec failed: %s", querybuf.data);
+ elogif(Debug_print_ivm, INFO, "IVM apply_old_delta_with_count update: %s", querybuf.data);
+
+ resetStringInfo(&querybuf);
+ appendStringInfo(&querybuf,
+ "DELETE FROM %s AS mv USING t "
+ "WHERE mv.ctid OPERATOR(pg_catalog.=) t.tid"
+ " AND mv.gp_segment_id OPERATOR(pg_catalog.=) t.gid"
+ " AND for_dlt",
+ matviewname);
+#endif
+ if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE)
+ elog(ERROR, "SPI_exec failed: %s", querybuf.data);
+ elogif(Debug_print_ivm, INFO, "IVM apply_old_delta_with_count delete: %s", querybuf.data);
+
+ /* Clean up temp tables. */
+ resetStringInfo(&querybuf);
+ appendStringInfo(&querybuf, "DROP TABLE t");
+ if (SPI_exec(querybuf.data, 0) != SPI_OK_UTILITY)
+ elog(ERROR, "SPI_exec failed: %s", querybuf.data);
+}
+
+/*
+ * apply_old_delta
+ *
+ * Execute a query for applying a delta table given by deltname_old
+ * which contains tuples to be deleted from to a materialized view given by
+ * matviewname. This is used when counting is not required.
+ */
+static void
+apply_old_delta(const char *matviewname, const char *deltaname_old,
+ List *keys)
+{
+ StringInfoData querybuf;
+ StringInfoData keysbuf;
+ char *match_cond;
+ ListCell *lc;
+
+ /* build WHERE condition for searching tuples to be deleted */
+ match_cond = get_matching_condition_string(keys);
+
+ /* build string of keys list */
+ initStringInfo(&keysbuf);
+ foreach (lc, keys)
+ {
+ Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc);
+ char *resname = NameStr(attr->attname);
+ appendStringInfo(&keysbuf, "%s", quote_qualified_identifier("mv", resname));
+ if (lnext(keys, lc))
+ appendStringInfo(&keysbuf, ", ");
+ }
+
+ /* Search for matching tuples from the view and update or delete if found. */
+ initStringInfo(&querybuf);
+ appendStringInfo(&querybuf,
+ "DELETE FROM %s WHERE (ctid, gp_segment_id) IN ("
+ "SELECT tid, sid FROM (SELECT pg_catalog.row_number() over (partition by %s) AS \"__ivm_row_number__\","
+ "mv.ctid AS tid, mv.gp_segment_id as sid,"
+ "diff.\"__ivm_count__\""
+ "FROM %s AS mv, %s AS diff "
+ "WHERE %s) v "
+ "WHERE v.\"__ivm_row_number__\" OPERATOR(pg_catalog.<=) v.\"__ivm_count__\")",
+ matviewname,
+ keysbuf.data,
+ matviewname, deltaname_old,
+ match_cond);
+
+ if (SPI_exec(querybuf.data, 0) != SPI_OK_DELETE)
+ elog(ERROR, "SPI_exec failed: %s", querybuf.data);
+ elogif(Debug_print_ivm, INFO, "IVM apply_old_delta delete: %s", querybuf.data);
+}
+
+/*
+ * apply_new_delta_with_count
+ *
+ * Execute a query for applying a delta table given by deltname_new
+ * which contains tuples to be inserted into a materialized view given by
+ * matviewname. This is used when counting is required, that is, the view
+ * has aggregate or distinct. Also, when a table in EXISTS sub queries
+ * is modified.
+ *
+ * If the view desn't have aggregates or has GROUP BY, this requires a keys
+ * list to identify a tuple in the view. If the view has aggregates, this
+ * requires strings representing SET clause for updating aggregate values.
+ */
+static void
+apply_new_delta_with_count(const char *matviewname, const char* deltaname_new,
+ List *keys, StringInfo aggs_set, StringInfo target_list,
+ const char* count_colname)
+{
+ StringInfoData querybuf;
+ StringInfoData returning_keys;
+ ListCell *lc;
+ char *match_cond = "";
+
+ /* build WHERE condition for searching tuples to be updated */
+ match_cond = get_matching_condition_string(keys);
+
+ /* build string of keys list */
+ initStringInfo(&returning_keys);
+ if (keys)
+ {
+ foreach (lc, keys)
+ {
+ Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc);
+ char *resname = NameStr(attr->attname);
+ appendStringInfo(&returning_keys, "%s", quote_qualified_identifier("mv", resname));
+ if (lnext(keys, lc))
+ appendStringInfo(&returning_keys, ", ");
+ }
+ }
+ else
+ appendStringInfo(&returning_keys, "NULL");
+
+ /* Search for matching tuples from the view and update if found or insert if not. */
+ initStringInfo(&querybuf);
+#if 0
+ appendStringInfo(&querybuf,
+ "WITH updt AS (" /* update a tuple if this exists in the view */
+ "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s "
+ "%s " /* SET clauses for aggregates */
+ "FROM %s AS diff "
+ "WHERE %s " /* tuple matching condition */
+ "RETURNING %s" /* returning keys of updated tuples */
+ ") INSERT INTO %s (%s)" /* insert a new tuple if this doesn't existw */
+ "SELECT %s FROM %s AS diff "
+ "WHERE NOT EXISTS (SELECT 1 FROM updt AS mv WHERE %s);",
+ matviewname, count_colname, count_colname, count_colname,
+ (aggs_set != NULL ? aggs_set->data : ""),
+ deltaname_new,
+ match_cond,
+ returning_keys.data,
+ matviewname, target_list->data,
+ target_list->data, deltaname_new,
+ match_cond);
+#else
+ appendStringInfo(&querybuf,
+ "UPDATE %s AS mv SET %s = mv.%s OPERATOR(pg_catalog.+) diff.%s "
+ "%s " /* SET clauses for aggregates */
+ "FROM %s AS diff "
+ "WHERE %s ", /* tuple matching condition */
+ matviewname, count_colname, count_colname, count_colname,
+ (aggs_set != NULL ? aggs_set->data : ""),
+ deltaname_new,
+ match_cond);
+ if (SPI_exec(querybuf.data, 0) != SPI_OK_UPDATE)
+ elog(ERROR, "SPI_exec failed: %s", querybuf.data);
+ elogif(Debug_print_ivm, INFO, "IVM apply_new_delta_with_count update: %s", querybuf.data);
+
+ resetStringInfo(&querybuf);
+ appendStringInfo(&querybuf,
+ "INSERT INTO %s (%s)" /* insert a new tuple if this doesn't existw */
+ "SELECT %s FROM %s AS diff "
+ "WHERE NOT EXISTS (SELECT 1 FROM %s AS mv WHERE %s);",
+ matviewname, target_list->data,
+ target_list->data, deltaname_new,
+ matviewname, match_cond);
+#endif
+ if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT)
+ elog(ERROR, "SPI_exec failed: %s", querybuf.data);
+ elogif(Debug_print_ivm,INFO, "IVM apply_new_delta_with_count insert: %s", querybuf.data);
+}
+
+/*
+ * apply_new_delta
+ *
+ * Execute a query for applying a delta table given by deltname_new
+ * which contains tuples to be inserted into a materialized view given by
+ * matviewname. This is used when counting is not required.
+ */
+static void
+apply_new_delta(const char *matviewname, const char *deltaname_new,
+ StringInfo target_list)
+{
+ StringInfoData querybuf;
+
+ /* Search for matching tuples from the view and update or delete if found. */
+ initStringInfo(&querybuf);
+
+ appendStringInfo(&querybuf,
+ "INSERT INTO %s (%s) SELECT %s FROM ("
+ "SELECT diff.*, pg_catalog.generate_series(1, diff.\"__ivm_count__\")"
+ " AS __ivm_generate_series__ "
+ "FROM %s AS diff) AS v",
+ matviewname, target_list->data, target_list->data,
+ deltaname_new);
+
+ if (SPI_exec(querybuf.data, 0) != SPI_OK_INSERT)
+ elog(ERROR, "SPI_exec failed: %s", querybuf.data);
+ elogif(Debug_print_ivm, INFO, "IVM apply_new_delta: %s", querybuf.data);
+}
+
+/*
+ * get_matching_condition_string
+ *
+ * Build a predicate string for looking for a tuple with given keys.
+ */
+static char *
+get_matching_condition_string(List *keys)
+{
+ StringInfoData match_cond;
+ ListCell *lc;
+
+ /* If there is no key columns, the condition is always true. */
+ if (keys == NIL)
+ return "true";
+
+ initStringInfo(&match_cond);
+ foreach (lc, keys)
+ {
+ Form_pg_attribute attr = (Form_pg_attribute) lfirst(lc);
+ char *resname = NameStr(attr->attname);
+ char *mv_resname = quote_qualified_identifier("mv", resname);
+ char *diff_resname = quote_qualified_identifier("diff", resname);
+ Oid typid = attr->atttypid;
+
+ /* Considering NULL values, we can not use simple = operator. */
+ appendStringInfo(&match_cond, "(");
+ generate_equal(&match_cond, typid, mv_resname, diff_resname);
+ appendStringInfo(&match_cond, " OR (%s IS NULL AND %s IS NULL))",
+ mv_resname, diff_resname);
+
+ if (lnext(keys, lc))
+ appendStringInfo(&match_cond, " AND ");
+ }
+
+ return match_cond.data;
+}
+
+/*
+ * generate_equals
+ *
+ * Generate an equality clause using given operands' default equality
+ * operator.
+ */
+static void
+generate_equal(StringInfo querybuf, Oid opttype,
+ const char *leftop, const char *rightop)
+{
+ TypeCacheEntry *typentry;
+
+ typentry = lookup_type_cache(opttype, TYPECACHE_EQ_OPR);
+ if (!OidIsValid(typentry->eq_opr))
+ ereport(ERROR,
+ (errcode(ERRCODE_UNDEFINED_FUNCTION),
+ errmsg("could not identify an equality operator for type %s",
+ format_type_be_qualified(opttype))));
+
+ generate_operator_clause(querybuf,
+ leftop, opttype,
+ typentry->eq_opr,
+ rightop, opttype);
+}
+
+/*
+ * mv_InitHashTables
+ */
+void
+mv_InitHashTables(void)
+{
+ HASHCTL info, ctl;
+
+ memset(&info, 0, sizeof(info));
+ info.keysize = sizeof(Oid);
+ info.entrysize = sizeof(MV_TriggerHashEntry);
+ mv_trigger_info = hash_create("MV trigger info",
+ MV_INIT_QUERYHASHSIZE,
+ &info, HASH_ELEM | HASH_BLOBS);
+
+ memset(&ctl, 0, sizeof(ctl));
+ ctl.keysize = SNAPSHOT_KEYSIZE;
+ ctl.entrysize = sizeof(SnapshotDumpEntry);
+ ctl.hash = string_hash;
+ mv_trigger_snapshot = ShmemInitHash("MV trigger snapshot",
+ MV_INIT_SNAPSHOTHASHSIZE,
+ MV_INIT_SNAPSHOTHASHSIZE,
+ &ctl, HASH_ELEM | HASH_FUNCTION);
+
+}
+
+Size
+mv_TableShmemSize(void)
+{
+ return hash_estimate_size(MV_INIT_SNAPSHOTHASHSIZE, sizeof(SnapshotDumpEntry));
+}
+
+
+/*
+ * AtAbort_IVM
+ *
+ * Clean up hash entries for all materialized views. This is called at
+ * transaction abort.
+ */
+void
+AtAbort_IVM()
+{
+ AtEOXact_IVM(false);
+}
+
+void
+AtEOXact_IVM(bool isCommit)
+{
+ ListCell *lc;
+ List *mvlist = AfterTriggerGetMvList();
+
+ foreach(lc, mvlist)
+ {
+ Oid matviewOid = lfirst_oid(lc);
+ {
+ MV_TriggerHashEntry *entry = (MV_TriggerHashEntry *) hash_search(mv_trigger_info,
+ (void *) &matviewOid,
+ HASH_FIND, NULL);
+ if (entry != NULL)
+ {
+ entry->reference--;
+ ResourceOwner resowner = entry->resowner;
+ MemoryContext ctx = entry->context;
+ if (entry->reference == 0)
+ {
+ clean_up_IVM_hash_entry(entry, isCommit);
+ }
+ if (resowner)
+ {
+ ResourceOwnerRelease(resowner,
+ RESOURCE_RELEASE_BEFORE_LOCKS,
+ false, /* isCommit */
+ false); /* isTopLevel */
+ ResourceOwnerRelease(resowner,
+ RESOURCE_RELEASE_LOCKS,
+ false, /* isCommit */
+ false); /* isTopLevel */
+ ResourceOwnerRelease(resowner,
+ RESOURCE_RELEASE_AFTER_LOCKS,
+ false, /* isCommit */
+ false); /* isTopLevel */
+ ResourceOwnerDelete(resowner);
+ }
+ if (ctx)
+ {
+ MemoryContextReset(ctx);
+ MemoryContextDelete(ctx);
+ }
+ }
+ }
+ }
+}
+
+/*
+ * clean_up_IVM_hash_entry
+ *
+ * Clean up tuple stores and hash entries for a materialized view after its
+ * maintenance finished.
+ */
+static void
+clean_up_IVM_hash_entry(MV_TriggerHashEntry *entry, bool is_abort)
+{
+ ListCell *lc;
+
+ foreach(lc, entry->tables)
+ {
+ MV_TriggerTable *table = (MV_TriggerTable *) lfirst(lc);
+
+ if (table->old_tuplestores)
+ {
+ list_free(table->old_tuplestores);
+ table->old_tuplestores = NIL;
+ }
+ if (table->new_tuplestores)
+ {
+ list_free(table->new_tuplestores);
+ table->new_tuplestores = NIL;
+ }
+ if (!is_abort)
+ {
+ if (table->slot)
+ {
+ ExecDropSingleTupleTableSlot(table->slot);
+ table->slot = NULL;
+ }
+ if (table->rel)
+ {
+ table_close(table->rel, NoLock);
+ table->rel = NULL;
+ }
+ }
+ }
+
+ if (entry->tables)
+ {
+ list_free(entry->tables);
+ entry->tables = NIL;
+ }
+ clean_up_ivm_dsm_entry(entry);
+
+ hash_search(mv_trigger_info, (void *) &entry->matview_id, HASH_REMOVE, NULL);
+}
+
+/*
+ * clean_up_ivm_dsm_entry
+ *
+ * This function is responsible for cleaning up the DSM entry associated with
+ * a given materialized view trigger hash entry.
+ */
+static void
+clean_up_ivm_dsm_entry(MV_TriggerHashEntry *entry)
+{
+ SnapshotDumpEntry *pDump;
+ if (entry->snapname && entry->snapname[0] != '\0' && Gp_is_writer)
+ {
+ bool found;
+ LWLockAcquire(GPIVMResLock, LW_EXCLUSIVE);
+ pDump = (SnapshotDumpEntry *) hash_search(mv_trigger_snapshot,
+ (void *)entry->snapname,
+ HASH_FIND, &found);
+ if (found)
+ {
+ /* Only creater can detach the dsm segment. */
+ if (pDump->handle && pDump->pid == MyProcPid)
+ {
+ Assert(entry->matview_id == pDump->matview_id);
+ dsm_detach(pDump->segment);
+ pDump->handle = DSM_HANDLE_INVALID;
+ }
+ hash_search(mv_trigger_snapshot, (void *)entry->snapname, HASH_REMOVE, NULL);
+ entry->snapname = NULL;
+ }
+ LWLockRelease(GPIVMResLock);
+ }
+}
+
+/*
+ * isIvmName
+ *
+ * Check if this is a IVM hidden column from the name.
+ */
+bool
+isIvmName(const char *s)
+{
+ if (s)
+ return (strncmp(s, "__ivm_", 6) == 0);
+ return false;
+}
+
+/*
+ * This function is responsible for refreshing the materialized view.
+ * It fills the memory with the result of the query execution.
+ * The result is then sent to the specified destination receiver.
+ */
+static uint64
+refresh_matview_memoryfill(DestReceiver *dest, Query *query,
+ QueryEnvironment *queryEnv,
+ TupleDesc *resultTupleDesc,
+ const char *queryString, Relation matviewRel)
+{
+ List *rewritten;
+ PlannedStmt *plan;
+ QueryDesc *queryDesc;
+ Query *copied_query;
+ uint64 processed;
+
+ List *saved_dispatch_oids = GetAssignedOidsForDispatch();
+
+ /* Lock and rewrite, using a copy to preserve the original query. */
+ copied_query = copyObject(query);
+ AcquireRewriteLocks(copied_query, true, false);
+ rewritten = QueryRewrite(copied_query);
+
+ /* SELECT should never rewrite to more or less than one SELECT query */
+ if (list_length(rewritten) != 1)
+ elog(ERROR, "unexpected rewrite result for refresh_matview_memoryfill.");
+ query = (Query *) linitial(rewritten);
+ query->parentStmtType = PARENTSTMTTYPE_CTAS;
+
+ query->intoPolicy = matviewRel->rd_cdbpolicy;
+
+ /* Check for user-requested abort. */
+ CHECK_FOR_INTERRUPTS();
+ /* Close parallel insert to tuplestore. */
+ plan = pg_plan_query(query, queryString, CURSOR_OPT_PARALLEL_OK, NULL);
+
+ plan->intoClause = makeIvmIntoClause(queryString, matviewRel);
+ plan->refreshClause = NULL;
+ /*
+ * Use a snapshot with an updated command ID to ensure this query sees
+ * results of any previously executed queries. (This could only matter if
+ * the planner executed an allegedly-stable function that changed the
+ * database contents, but let's do it anyway to be safe.)
+ */
+ PushCopiedSnapshot(GetActiveSnapshot());
+ UpdateActiveSnapshotCommandId();
+
+ /* Create a QueryDesc, redirecting output to our tuple receiver */
+ queryDesc = CreateQueryDesc(plan, queryString,
+ GetActiveSnapshot(), InvalidSnapshot,
+ dest, NULL, queryEnv ? queryEnv: NULL, 0);
+
+ RestoreOidAssignments(saved_dispatch_oids);
+
+ /* call ExecutorStart to prepare the plan for execution */
+ ExecutorStart(queryDesc, 0);
+
+ /* run the plan */
+ ExecutorRun(queryDesc, ForwardScanDirection, 0L, true);
+
+ if (resultTupleDesc)
+ *resultTupleDesc = CreateTupleDescCopy(queryDesc->tupDesc);
+
+ /* and clean up */
+ ExecutorFinish(queryDesc);
+ ExecutorEnd(queryDesc);
+
+ processed = queryDesc->es_processed;
+
+ FreeQueryDesc(queryDesc);
+
+ PopActiveSnapshot();
+
+ elogif(Debug_print_ivm, INFO, "IVM processed %s, %lu tuples.", queryString, processed);
+
+ return processed;
+}
+
+/*
+ *
+ * Add a preassigned materialized view entry to the hash table.
+ */
+void
+AddPreassignedMVEntry(Oid matview_id, Oid table_id, const char* snapname)
+{
+ MV_TriggerTable *table;
+ bool found;
+ ListCell *lc;
+ MV_TriggerHashEntry *entry;
+ MemoryContext oldcxt;
+ ResourceOwner oldowner;
+ int len = strlen(snapname);
+
+ entry = (MV_TriggerHashEntry *) hash_search(mv_trigger_info,
+ (void *) &matview_id,
+ HASH_ENTER, &found);
+ if (!found)
+ {
+ entry->context = AllocSetContextCreate(TopMemoryContext,
+ "IVM Reader Session",
+ ALLOCSET_DEFAULT_SIZES);
+ entry->resowner = ResourceOwnerCreate(TopTransactionResourceOwner, "IVM Reader Session");
+ entry->matview_id = matview_id;
+ entry->reference = 1;
+ entry->tables = NIL;
+ entry->has_old = false;
+ entry->has_new = false;
+ entry->pid = MyProcPid;
+ entry->snapshot = NULL;
+ }
+
+ /* Switch to the new resource owner and memory context */
+ oldowner = CurrentResourceOwner;
+ CurrentResourceOwner = entry->resowner;
+ oldcxt = MemoryContextSwitchTo(entry->context);
+
+ /* Copy the snapshot name */
+ if (entry->snapname)
+ strncpy(entry->snapname, snapname, len);
+ else
+ entry->snapname = pstrdup(snapname);
+ entry->snapname[len] = '\0';
+
+ /* Import the snapshot */
+ entry->snapshot = ivm_import_snapshot(snapname);
+
+ Assert(entry->snapshot);
+
+ found = false;
+ foreach(lc, entry->tables)
+ {
+ table = (MV_TriggerTable *) lfirst(lc);
+ if (table->table_id == table_id)
+ {
+ found = true;
+ break;
+ }
+ }
+ if (!found)
+ {
+ table = (MV_TriggerTable *) palloc0(sizeof(MV_TriggerTable));
+ entry->tables = lappend(entry->tables, table);
+ }
+ /* Switch back to the old memory context and resource owner */
+ MemoryContextSwitchTo(oldcxt);
+ CurrentResourceOwner = oldowner;
+
+ AfterTriggerAppendMvList(matview_id);
+ return;
+}
+
+/*
+ * ivm_immediate_cleanup
+ *
+ * Clean up hash entries for a materialized view after its
+ * maintenance finished.
+ */
+Datum
+ivm_immediate_cleanup(PG_FUNCTION_ARGS)
+{
+ Oid matview_id = PG_GETARG_OID(0);
+ bool result = true;
+ ResourceOwner resowner;
+ MemoryContext ctx;
+
+ MV_TriggerHashEntry *entry = (MV_TriggerHashEntry *) hash_search(mv_trigger_info,
+ (void *) &matview_id,
+ HASH_FIND, NULL);
+ if (entry != NULL)
+ {
+ resowner = entry->resowner;
+ ctx = entry->context;
+ clean_up_IVM_hash_entry(entry, true);
+ if (resowner)
+ {
+ ResourceOwnerRelease(resowner,
+ RESOURCE_RELEASE_BEFORE_LOCKS,
+ false, /* isCommit */
+ false); /* isTopLevel */
+ ResourceOwnerRelease(resowner,
+ RESOURCE_RELEASE_LOCKS,
+ false, /* isCommit */
+ false); /* isTopLevel */
+ ResourceOwnerRelease(resowner,
+ RESOURCE_RELEASE_AFTER_LOCKS,
+ false, /* isCommit */
+ false); /* isTopLevel */
+ }
+ if (ctx)
+ {
+ MemoryContextReset(ctx);
+ MemoryContextDelete(ctx);
+ }
+ }
+
+ PG_RETURN_BOOL(result);
+}
+
+/*
+ * apply_cleanup
+ *
+ * Apply immediate cleanup for a materialized view.
+ */
+static void
+apply_cleanup(Oid matview_id)
+{
+ StringInfoData querybuf;
+ /* Open SPI context. */
+ if (SPI_connect() != SPI_OK_CONNECT)
+ elog(ERROR, "SPI_connect failed");
+
+ initStringInfo(&querybuf);
+ appendStringInfo(&querybuf,
+ "SELECT pg_catalog.ivm_immediate_cleanup(%d::pg_catalog.oid)",
+ matview_id);
+
+ if (SPI_exec(querybuf.data, 0) != SPI_OK_SELECT)
+ elog(ERROR, "SPI_exec failed: %s", querybuf.data);
+
+ elogif(Debug_print_ivm, INFO, "IVM apply_cleanup: %s", querybuf.data);
+
+ /* Close SPI context. */
+ if (SPI_finish() != SPI_OK_FINISH)
+ elog(ERROR, "SPI_finish failed");
+
+ return;
+}
+
+/*
+ * ivm_export_snapshot
+ *
+ * Export a snapshot in QEs.
+ *
+ * Parameters:
+ * - matview_id: the OID of the materialized view
+ * - snapname: the name of the snapshot to export
+ */
+static void
+ivm_export_snapshot(Oid matview_id, char *snapname)
+{
+ StringInfoData querybuf;
+ /* Open SPI context. */
+ if (SPI_connect() != SPI_OK_CONNECT)
+ elog(ERROR, "SPI_connect failed");
+
+ initStringInfo(&querybuf);
+ appendStringInfo(&querybuf,
+ "SELECT pg_catalog.pg_export_snapshot_def(%d::pg_catalog.oid, '%s')", matview_id, snapname);
+
+ if (SPI_exec(querybuf.data, 0) != SPI_OK_SELECT)
+ elog(ERROR, "SPI_exec failed: %s", querybuf.data);
+
+ elogif(Debug_print_ivm, INFO, "IVM ivm_export_snapshot: %s", querybuf.data);
+
+ /* Close SPI context. */
+ if (SPI_finish() != SPI_OK_FINISH)
+ elog(ERROR, "SPI_finish failed");
+ return;
+}
+
+/*
+ * pg_export_snapshot_def
+ * SQL-callable wrapper for export snapshot.
+ */
+Datum
+pg_export_snapshot_def(PG_FUNCTION_ARGS)
+{
+ bool found;
+ Oid matview_id = PG_GETARG_OID(0);
+ char *snapname = text_to_cstring(PG_GETARG_TEXT_PP(1));
+ Snapshot snapshot = GetActiveSnapshot();
+ SnapshotDumpEntry *pDump;
+ dsm_segment *segment;
+ Assert(snapshot->snapshot_type == SNAPSHOT_MVCC);
+ Assert(snapname);
+
+ LWLockAcquire(GPIVMResLock, LW_EXCLUSIVE);
+ pDump = (SnapshotDumpEntry *) hash_search(mv_trigger_snapshot,
+ (void *) snapname,
+ HASH_ENTER_NULL,
+ &found);
+ if (!pDump)
+ ereport(ERROR,
+ (errcode(ERRCODE_OUT_OF_MEMORY),
+ errmsg("out of shared memory"),
+ errhint("out of cross-slice trigger snapshot slots.")));
+
+ if (!found)
+ {
+ Size sz = EstimateSnapshotSpace(snapshot);
+ segment = dsm_create(sz, 0);
+
+ char *ptr = dsm_segment_address(segment);
+ SerializeSnapshot(snapshot, ptr);
+
+ pDump->segment = segment;
+ pDump->handle = dsm_segment_handle(segment);
+ pDump->pid = MyProcPid;
+ pDump->matview_id = matview_id;
+
+ dsm_pin_mapping(segment);
+ }
+ LWLockRelease(GPIVMResLock);
+
+ PG_RETURN_TEXT_P(cstring_to_text(snapname));
+}
+
+
+/*
+ * ExecuteTruncateGuts_IVM
+ *
+ * This function truncates the given materialized view and its dependent
+ * objects, and then regenerates the data for the materialized view.
+ *
+ * Parameters:
+ * - matviewRel: the relation of the materialized view to truncate
+ * - matviewOid: the OID of the materialized view to truncate
+ * - query: the query to use to regenerate the data for the materialized view
+ */
+static void
+ExecuteTruncateGuts_IVM(Relation matviewRel,
+ Oid matviewOid,
+ Query *query)
+{
+ Oid OIDNewHeap;
+ DestReceiver *dest;
+ uint64 processed = 0;
+ Query *dataQuery = rewriteQueryForIMMV(query, NIL);
+ char relpersistence = matviewRel->rd_rel->relpersistence;
+ RefreshClause *refreshClause;
+ /*
+ * 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).
+ */
+ OIDNewHeap = make_new_heap(matviewOid, matviewRel->rd_rel->reltablespace,
+ relpersistence, ExclusiveLock, false, true);
+ LockRelationOid(OIDNewHeap, AccessExclusiveLock);
+ dest = CreateTransientRelDestReceiver(OIDNewHeap, matviewOid, false,
+ relpersistence, false);
+
+ RangeVar *relation = makeRangeVar(get_namespace_name(get_rel_namespace(matviewOid)),
+ get_rel_name(matviewOid), -1);
+ refreshClause = MakeRefreshClause(false, false, relation);
+
+ dataQuery->intoPolicy = matviewRel->rd_cdbpolicy;
+ dataQuery->parentStmtType = PARENTSTMTTYPE_REFRESH_MATVIEW;
+ /* Generate the data */
+ processed = refresh_matview_datafill(dest, dataQuery, "truncate", refreshClause);
+ refresh_by_heap_swap(matviewOid, OIDNewHeap, relpersistence);
+}
+
+
+/*
+ * ivm_import_snapshot
+ *
+ * This function imports a snapshot from the hash table based on the given name.
+ *
+ * Parameters:
+ * - idstr: a string representing the id of the snapshot to import
+ *
+ * Returns:
+ * - the imported snapshot
+ */
+static Snapshot
+ivm_import_snapshot(const char *idstr)
+{
+ bool found;
+ SnapshotDumpEntry *pDump;
+ Snapshot snapshot = NULL;
+
+ LWLockAcquire(GPIVMResLock, LW_SHARED);
+ pDump = hash_search(mv_trigger_snapshot, (void*)idstr, HASH_FIND, &found);
+
+ Assert (found && pDump != NULL);
+ if (found)
+ {
+ Assert(pDump);
+ if (pDump->pid == MyProcPid)
+ {
+ char *ptr = dsm_segment_address(pDump->segment);
+ snapshot = RestoreSnapshot(ptr);
+ }
+ else
+ {
+ dsm_segment* segment = dsm_attach(pDump->handle);
+ char *ptr = dsm_segment_address(segment);
+ snapshot = RestoreSnapshot(ptr);
+ dsm_detach(segment);
+ }
+ }
+ LWLockRelease(GPIVMResLock);
+
+ return snapshot;
+}
+
+/*
+ * makeIvmIntoClause
+ *
+ * This function creates an IntoClause node for IVM (Incremental View Maintenance).
+ * It sets the 'ivm' field to true, 'rel' field to NULL, 'enrname' field to the input enrname,
+ * 'distributedBy' field to the distribution policy of the input relation (matviewRel), and 'matviewOid' field
+ * to the OID of the input relation.
+ *
+ * Parameters:
+ * - enrname: a string representing the named tuplestore
+ * - matviewRel: a relation object representing the materialized view
+ *
+ * Returns:
+ * - a pointer to the created IntoClause node
+ */
+static IntoClause*
+makeIvmIntoClause(const char *enrname, Relation matviewRel)
+{
+ IntoClause *intoClause = makeNode(IntoClause);
+ intoClause->ivm = true;
+ /* rel is NULL means put tuples into memory.*/
+ intoClause->rel = NULL;
+ intoClause->enrname = (char*) enrname;
+ intoClause->distributedBy = (Node*) make_distributedby_for_rel(matviewRel);
+ intoClause->matviewOid = RelationGetRelid(matviewRel);
+ return intoClause;
+}
+
+/*
+ * transientenr_init
+ *
+ * This function initializes the transientenr.
+ *
+ * Parameters:
+ * - queryDesc: the query descriptor
+ */
+void
+transientenr_init(QueryDesc *queryDesc)
+{
+ MV_TriggerHashEntry *entry;
+ bool found;
+ IntoClause *into = queryDesc->plannedstmt->intoClause;
+ Oid matviewOid = into->matviewOid;
+
+ entry = (MV_TriggerHashEntry *) hash_search(mv_trigger_info,
+ (void *) &matviewOid,
+ HASH_FIND, &found);
+ Assert (found && entry != NULL);
+ Assert (into->ivm);
+
+ queryDesc->dest = CreateDestReceiver(DestPersistentstore);
+ SetPersistentTstoreDestReceiverParams(queryDesc->dest,
+ NULL,
+ entry->resowner,
+ entry->context,
+ true,
+ into->enrname);
+}
+
+/*
+ * ivm_set_ts_persitent_name
+ *
+ * This function sets the transition table file name for the IVM trigger.
+ */
+static void
+ivm_set_ts_persitent_name(TriggerData *trigdata, Oid relid, Oid mvid)
+{
+ MemoryContext oldctx;
+ CmdType cmd;
+
+ if (TRIGGER_FIRED_BY_INSERT(trigdata->tg_event))
+ cmd = CMD_INSERT;
+ else if (TRIGGER_FIRED_BY_DELETE(trigdata->tg_event))
+ cmd = CMD_DELETE;
+ else if (TRIGGER_FIRED_BY_UPDATE(trigdata->tg_event))
+ cmd = CMD_UPDATE;
+ else
+ {
+ AfterTriggerAppendMvList(mvid);
+ return;
+ }
+ oldctx = MemoryContextSwitchTo(TopMemoryContext);
+ SetTransitionTableName(relid, cmd, mvid);
+ MemoryContextSwitchTo(oldctx);
+}
\ No newline at end of file
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 63e1ee76d87..3035aad402a 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -66,6 +66,7 @@
#include "commands/comment.h"
#include "commands/createas.h"
#include "commands/defrem.h"
+#include "commands/matview.h"
#include "commands/event_trigger.h"
#include "commands/policy.h"
#include "commands/sequence.h"
@@ -3848,6 +3849,14 @@ renameatt_internal(Oid myrelid,
targetrelation = relation_open(myrelid, AccessExclusiveLock);
renameatt_check(myrelid, RelationGetForm(targetrelation), recursing);
+ /*
+ * Don't rename IVM columns.
+ */
+ if (RelationIsIVM(targetrelation) && isIvmName(oldattname))
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("IVM column can not be renamed")));
+
/*
* if the 'recurse' flag is set then we are supposed to rename this
* attribute in all classes that inherit from 'relname' (as well as in
diff --git a/src/backend/commands/trigger.c b/src/backend/commands/trigger.c
index 023624dbd22..82b12c3fa97 100644
--- a/src/backend/commands/trigger.c
+++ b/src/backend/commands/trigger.c
@@ -100,7 +100,7 @@ static void AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
static void AfterTriggerEnlargeQueryState(void);
static bool before_stmt_triggers_fired(Oid relid, CmdType cmdType);
-
+static char* make_delta_ts_name(const char *prefix, Oid relid, int count);
/*
* Create a trigger. Returns the address of the created trigger.
*
@@ -2273,12 +2273,6 @@ ExecBSInsertTriggers(EState *estate, ResultRelInfo *relinfo)
int i;
TriggerData LocTriggerData = {0};
- if (Gp_role == GP_ROLE_EXECUTE)
- {
- /* Don't fire statement-triggers in executor nodes. */
- return;
- }
-
trigdesc = relinfo->ri_TrigDesc;
if (trigdesc == NULL)
@@ -2498,12 +2492,6 @@ ExecBSDeleteTriggers(EState *estate, ResultRelInfo *relinfo)
int i;
TriggerData LocTriggerData = {0};
- if (Gp_role == GP_ROLE_EXECUTE)
- {
- /* Don't fire statement-triggers in executor nodes. */
- return;
- }
-
trigdesc = relinfo->ri_TrigDesc;
if (trigdesc == NULL)
@@ -2737,12 +2725,6 @@ ExecBSUpdateTriggers(EState *estate, ResultRelInfo *relinfo)
TriggerData LocTriggerData = {0};
Bitmapset *updatedCols;
- if (Gp_role == GP_ROLE_EXECUTE)
- {
- /* Don't fire statement-triggers in executor nodes. */
- return;
- }
-
trigdesc = relinfo->ri_TrigDesc;
if (trigdesc == NULL)
@@ -3057,12 +3039,6 @@ ExecBSTruncateTriggers(EState *estate, ResultRelInfo *relinfo)
int i;
TriggerData LocTriggerData = {0};
- if (Gp_role == GP_ROLE_EXECUTE)
- {
- /* Don't fire statement-triggers in executor nodes. */
- return;
- }
-
trigdesc = relinfo->ri_TrigDesc;
if (trigdesc == NULL)
@@ -3552,6 +3528,10 @@ typedef struct AfterTriggerEventList
* end of the list, so it is relatively easy to discard them. The event
* list chunks themselves are stored in event_cxt.
*
+ * prolonged_tuplestored is a list of transition table tuplestores whose
+ * life are prolonged to the end of the outmost query instead of each nested
+ * query.
+ *
* query_depth is the current depth of nested AfterTriggerBeginQuery calls
* (-1 when the stack is empty).
*
@@ -3617,6 +3597,8 @@ typedef struct AfterTriggersData
SetConstraintState state; /* the active S C state */
AfterTriggerEventList events; /* deferred-event list */
MemoryContext event_cxt; /* memory context for events, if any */
+ List *prolonged_tuplestores; /* list of prolonged tuplestores */
+ List *mv_list; /* materialized view oids */
/* per-query-level data: */
AfterTriggersQueryData *query_stack; /* array of structs shown below */
@@ -3652,6 +3634,7 @@ struct AfterTriggersTableData
bool closed; /* true when no longer OK to add tuples */
bool before_trig_done; /* did we already queue BS triggers? */
bool after_trig_done; /* did we already queue AS triggers? */
+ bool prolonged; /* are transition tables prolonged? */
AfterTriggerEventList after_trig_events; /* if so, saved list pointer */
Tuplestorestate *old_tuplestore; /* "old" transition table, if any */
Tuplestorestate *new_tuplestore; /* "new" transition table, if any */
@@ -3674,6 +3657,7 @@ static AfterTriggersTableData *GetAfterTriggersTableData(Oid relid,
static TupleTableSlot *GetAfterTriggersStoreSlot(AfterTriggersTableData *table,
TupleDesc tupdesc);
static void AfterTriggerFreeQuery(AfterTriggersQueryData *qs);
+static void release_or_prolong_tuplestore(Tuplestorestate *ts, bool prolonged);
static SetConstraintState SetConstraintStateCreate(int numalloc);
static SetConstraintState SetConstraintStateCopy(SetConstraintState state);
static SetConstraintState SetConstraintStateAddItem(SetConstraintState state,
@@ -4432,6 +4416,116 @@ afterTriggerInvokeEvents(AfterTriggerEventList *events,
}
+/*
+ * SetTransitionTablePreserved
+ *
+ * Prolong lifespan of transition tables corresponding specified relid and
+ * command type to the end of the outmost query instead of each nested query.
+ * This enables to use nested AFTER trigger's transition tables from outer
+ * query's triggers. Currently, only immediate incremental view maintenance
+ * uses this.
+ */
+void
+SetTransitionTablePreserved(Oid relid, CmdType cmdType)
+{
+ AfterTriggersTableData *table;
+ AfterTriggersQueryData *qs;
+ bool found = false;
+ ListCell *lc;
+
+ /* Check state, like AfterTriggerSaveEvent. */
+ if (afterTriggers.query_depth < 0)
+ elog(ERROR, "SetTransitionTablePreserved() called outside of query");
+
+ qs = &afterTriggers.query_stack[afterTriggers.query_depth];
+
+ foreach(lc, qs->tables)
+ {
+ table = (AfterTriggersTableData *) lfirst(lc);
+ if (table->relid == relid && table->cmdType == cmdType &&
+ table->closed)
+ {
+ table->prolonged = true;
+ found = true;
+ }
+ }
+
+ if (!found)
+ elog(ERROR,"could not find table with OID %d and command type %d", relid, cmdType);
+}
+
+/*
+ * SetTransitionTableName
+ *
+ * Preassign tuplestore dump file name.
+ * Currently, only immediate incremental view maintenance
+ * uses this.
+ */
+void
+SetTransitionTableName(Oid relid, CmdType cmdType, Oid mvoid)
+{
+ AfterTriggersTableData *table;
+ AfterTriggersQueryData *qs;
+ bool found = false;
+ ListCell *lc;
+
+ /* Check state, like AfterTriggerSaveEvent. */
+ if (afterTriggers.query_depth < 0)
+ elog(ERROR, "SetTransitionTableName() called outside of query");
+
+ qs = &afterTriggers.query_stack[afterTriggers.query_depth];
+
+ foreach(lc, qs->tables)
+ {
+ table = (AfterTriggersTableData *) lfirst(lc);
+ if (table->relid == relid && table->cmdType == cmdType)
+ {
+ if (table->new_tuplestore)
+ {
+ char *name = make_delta_ts_name("new", relid, gp_command_count);
+ tuplestore_set_sharedname(table->new_tuplestore, name);
+ tuplestore_set_tableid(table->new_tuplestore, relid);
+ }
+ if (table->old_tuplestore)
+ {
+ char *name = make_delta_ts_name("old", relid, gp_command_count);
+ tuplestore_set_sharedname(table->old_tuplestore, name);
+ tuplestore_set_tableid(table->old_tuplestore, relid);
+ }
+ found = true;
+ }
+ }
+
+ AfterTriggerAppendMvList(mvoid);
+
+ if (!found)
+ elog(ERROR,"could not find table with OID %d and command type %d", relid, cmdType);
+}
+
+/*
+ * AfterTriggerGetMvList
+ *
+ * Get the list of materialized views oid triggered by ivm.
+ */
+List*
+AfterTriggerGetMvList(void)
+{
+ return afterTriggers.mv_list;
+}
+
+/*
+ * AfterTriggerAppendMvList
+ *
+ * Append the materialized view oid to the list triggered by ivm.
+ */
+void
+AfterTriggerAppendMvList(Oid matview_id)
+{
+ MemoryContext oldcxt = MemoryContextSwitchTo(TopMemoryContext);
+ afterTriggers.mv_list = list_append_unique_oid(afterTriggers.mv_list, matview_id);
+ MemoryContextSwitchTo(oldcxt);
+}
+
/*
* GetAfterTriggersTableData
*
@@ -4626,6 +4720,7 @@ AfterTriggerBeginXact(void)
*/
afterTriggers.firing_counter = (CommandId) 1; /* mustn't be 0 */
afterTriggers.query_depth = -1;
+ afterTriggers.prolonged_tuplestores = NIL;
/*
* Verify that there is no leftover state remaining. If these assertions
@@ -4786,11 +4881,11 @@ AfterTriggerFreeQuery(AfterTriggersQueryData *qs)
ts = table->old_tuplestore;
table->old_tuplestore = NULL;
if (ts)
- tuplestore_end(ts);
+ release_or_prolong_tuplestore(ts, table->prolonged);
ts = table->new_tuplestore;
table->new_tuplestore = NULL;
if (ts)
- tuplestore_end(ts);
+ release_or_prolong_tuplestore(ts, table->prolonged);
if (table->storeslot)
ExecDropSingleTupleTableSlot(table->storeslot);
}
@@ -4804,6 +4899,22 @@ AfterTriggerFreeQuery(AfterTriggersQueryData *qs)
list_free_deep(tables);
}
+/*
+ * Release the tuplestore, or append it to the prolonged tuplestores list.
+ */
+static void
+release_or_prolong_tuplestore(Tuplestorestate *ts, bool prolonged)
+{
+ if (prolonged && afterTriggers.query_depth >= 0)
+ {
+ MemoryContext oldcxt = MemoryContextSwitchTo(TopMemoryContext);
+ afterTriggers.prolonged_tuplestores = lappend(afterTriggers.prolonged_tuplestores, ts);
+ MemoryContextSwitchTo(oldcxt);
+ }
+ else
+ tuplestore_end(ts);
+}
+
/* ----------
* AfterTriggerFireDeferred()
@@ -4913,6 +5024,20 @@ AfterTriggerEndXact(bool isCommit)
/* No more afterTriggers manipulation until next transaction starts. */
afterTriggers.query_depth = -1;
+ {
+ ListCell *lc;
+ foreach(lc, afterTriggers.prolonged_tuplestores)
+ {
+ Tuplestorestate *ts = (Tuplestorestate *) lfirst(lc);
+ if (ts)
+ {
+ //elog(INFO, "AfterTriggerEndXact releasing tuplestore c:%d", isCommit);
+ tuplestore_end(ts);
+ }
+ }
+ list_free(afterTriggers.prolonged_tuplestores);
+ afterTriggers.prolonged_tuplestores = NIL;
+ }
}
/*
@@ -4976,6 +5101,7 @@ AfterTriggerEndSubXact(bool isCommit)
AfterTriggerEvent event;
AfterTriggerEventChunk *chunk;
CommandId subxact_firing_id;
+ ListCell *lc;
/*
* Pop the prior state if needed.
@@ -5059,6 +5185,16 @@ AfterTriggerEndSubXact(bool isCommit)
}
}
}
+ foreach(lc, afterTriggers.prolonged_tuplestores)
+ {
+ Tuplestorestate *ts = (Tuplestorestate *) lfirst(lc);
+ if (CurrentResourceOwner == tuplestore_get_resowner(ts))
+ {
+ //elog(INFO, "AfterTriggerEndSubXact releasing tuplestore c:%d", isCommit);
+ tuplestore_end(ts);
+ afterTriggers.prolonged_tuplestores = foreach_delete_current(afterTriggers.prolonged_tuplestores, lc);
+ }
+ }
}
/* ----------
@@ -5614,10 +5750,6 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
if (afterTriggers.query_depth < 0)
elog(ERROR, "AfterTriggerSaveEvent() called outside of query");
- /* Don't fire statement-triggers in executor nodes. */
- if (!row_trigger && Gp_role == GP_ROLE_EXECUTE)
- return;
-
/* Be sure we have enough space to record events at this query depth. */
if (afterTriggers.query_depth >= afterTriggers.maxquerydepth)
AfterTriggerEnlargeQueryState();
@@ -6037,3 +6169,15 @@ pg_trigger_depth(PG_FUNCTION_ARGS)
{
PG_RETURN_INT32(MyTriggerDepth);
}
+
+static char*
+make_delta_ts_name(const char *prefix, Oid relid, int count)
+{
+ char buf[NAMEDATALEN];
+ char *name;
+
+ snprintf(buf, NAMEDATALEN, "__ivm_%s_%u_%u", prefix, relid, count);
+ name = pstrdup(buf);
+
+ return name;
+}
diff --git a/src/backend/executor/execAmi.c b/src/backend/executor/execAmi.c
index 42e11836d60..90585a88251 100644
--- a/src/backend/executor/execAmi.c
+++ b/src/backend/executor/execAmi.c
@@ -799,6 +799,7 @@ ExecSquelchNode(PlanState *node)
case T_SampleScanState:
case T_GatherState:
case T_GatherMergeState:
+ case T_NamedTuplestoreScanState:
break;
/*
diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c
index 233411c966d..0b5dfd03a3d 100644
--- a/src/backend/executor/execMain.c
+++ b/src/backend/executor/execMain.c
@@ -39,6 +39,7 @@
*-------------------------------------------------------------------------
*/
#include "postgres.h"
+#include "pgstat.h"
#include "access/heapam.h"
#include "access/htup_details.h"
@@ -52,6 +53,7 @@
#include "commands/tablespace.h"
#include "commands/trigger.h"
#include "executor/execdebug.h"
+#include "executor/nodeModifyTable.h"
#include "executor/nodeSubplan.h"
#include "foreign/fdwapi.h"
#include "jit/jit.h"
@@ -70,9 +72,9 @@
#include "utils/ruleutils.h"
#include "utils/snapmgr.h"
#include "utils/metrics_utils.h"
+#include "utils/queryenvironment.h"
#include "utils/ps_status.h"
-#include "utils/snapmgr.h"
#include "utils/typcache.h"
#include "utils/workfile_mgr.h"
#include "utils/faultinjector.h"
@@ -310,6 +312,13 @@ standard_ExecutorStart(QueryDesc *queryDesc, int eflags)
/*
* Fill in the query environment, if any, from queryDesc.
*/
+ if (queryDesc->ddesc && queryDesc->ddesc->namedRelList)
+ {
+ if (queryDesc->queryEnv == NULL)
+ queryDesc->queryEnv = create_queryEnv();
+ /* Update environment */
+ AddPreassignedENR(queryDesc->queryEnv, queryDesc->ddesc->namedRelList);
+ }
estate->es_queryEnv = queryDesc->queryEnv;
/*
@@ -583,6 +592,7 @@ standard_ExecutorStart(QueryDesc *queryDesc, int eflags)
if (queryDesc->ddesc != NULL)
{
queryDesc->ddesc->sliceTable = estate->es_sliceTable;
+ FillQueryDispatchDesc(estate->es_queryEnv, queryDesc->ddesc);
/*
* For CTAS querys that contain initplan, we need to copy a new oid dispatch list,
* since the preprocess_initplan will start a subtransaction, and if it's rollbacked,
@@ -762,7 +772,7 @@ standard_ExecutorRun(QueryDesc *queryDesc,
bool sendTuples;
MemoryContext oldcontext;
EndpointExecState *endpointExecState = NULL;
-
+ uint64 es_processed = 0;
/*
* NOTE: Any local vars that are set in the PG_TRY block and examined in the
* PG_CATCH block should be declared 'volatile'. (setjmp shenanigans)
@@ -937,11 +947,17 @@ standard_ExecutorRun(QueryDesc *queryDesc,
/* should never happen */
Assert(!"undefined parallel execution strategy");
}
+ if ((exec_identity == GP_IGNORE || exec_identity == GP_ROOT_SLICE) && operation != CMD_SELECT)
+ es_processed = mppExecutorWait(queryDesc);
}
PG_CATCH();
{
/* Close down interconnect etc. */
mppExecutorCleanup(queryDesc);
+ if (list_length(queryDesc->plannedstmt->paramExecTypes) > 0)
+ {
+ postprocess_initplans(queryDesc);
+ }
PG_RE_THROW();
}
PG_END_TRY();
@@ -981,7 +997,8 @@ standard_ExecutorRun(QueryDesc *queryDesc,
if (sendTuples)
dest->rShutdown(dest);
-
+ if (es_processed)
+ estate->es_processed = es_processed;
if (queryDesc->totaltime)
InstrStopNode(queryDesc->totaltime, estate->es_processed);
@@ -1502,8 +1519,8 @@ ExecCheckXactReadOnly(PlannedStmt *plannedstmt)
*/
if (plannedstmt->intoClause != NULL)
{
- Assert(plannedstmt->intoClause->rel);
- if (plannedstmt->intoClause->rel->relpersistence == RELPERSISTENCE_TEMP)
+ if ((plannedstmt->intoClause->rel && plannedstmt->intoClause->rel->relpersistence == RELPERSISTENCE_TEMP)
+ || (plannedstmt->intoClause->rel == NULL && plannedstmt->intoClause->ivm))
ExecutorMarkTransactionDoesWrites();
else
PreventCommandIfReadOnly(CreateCommandName((Node *) plannedstmt));
@@ -1870,7 +1887,15 @@ InitPlan(QueryDesc *queryDesc, int eflags)
* Also need to create tables in advance.
*/
if (queryDesc->plannedstmt->intoClause != NULL)
- intorel_initplan(queryDesc, eflags);
+ {
+ if (queryDesc->plannedstmt->intoClause->rel)
+ intorel_initplan(queryDesc, eflags);
+ if (queryDesc->plannedstmt->intoClause->rel == NULL &&
+ queryDesc->plannedstmt->intoClause->ivm && Gp_role == GP_ROLE_EXECUTE)
+ {
+ transientenr_init(queryDesc);
+ }
+ }
else if(queryDesc->plannedstmt->copyIntoClause != NULL)
{
queryDesc->dest = CreateCopyDestReceiver();
@@ -2315,6 +2340,18 @@ ExecPostprocessPlan(EState *estate)
*/
estate->es_direction = ForwardScanDirection;
+ if (Gp_role == GP_ROLE_DISPATCH)
+ {
+ /* Fire after triggers. */
+ foreach(lc, estate->es_auxmodifytables)
+ {
+ PlanState *ps = (PlanState *) lfirst(lc);
+ ModifyTableState *node = castNode(ModifyTableState, ps);
+
+ fireASTriggers(node);
+ }
+ return;
+ }
/*
* Run any secondary ModifyTable nodes to completion, in case the main
* query did not fetch all rows from them. (We do this to ensure that
diff --git a/src/backend/executor/execUtils.c b/src/backend/executor/execUtils.c
index 96afb82618d..9f60655a5ee 100644
--- a/src/backend/executor/execUtils.c
+++ b/src/backend/executor/execUtils.c
@@ -90,6 +90,7 @@
#include "miscadmin.h"
#include "nodes/makefuncs.h"
#include "storage/ipc.h"
+#include "cdb/cdbexplain.h"
#include "cdb/cdbllize.h"
#include "utils/guc.h"
#include "utils/workfile_mgr.h"
@@ -2080,6 +2081,92 @@ void mppExecutorFinishup(QueryDesc *queryDesc)
}
}
+/*
+ * QD wait for QEs to finish and check their results.
+ */
+uint64 mppExecutorWait(QueryDesc *queryDesc)
+{
+ EState *estate;
+ ExecSlice *currentSlice;
+ int primaryWriterSliceIndex;
+ uint64 es_processed = 0;
+
+ /* caller must have switched into per-query memory context already */
+ estate = queryDesc->estate;
+
+ currentSlice = getCurrentSlice(estate, LocallyExecutingSliceIndex(estate));
+ primaryWriterSliceIndex = PrimaryWriterSliceIndex(estate);
+
+ /*
+ * If QD, wait for QEs to finish and check their results.
+ */
+ if (estate->dispatcherState && estate->dispatcherState->primaryResults)
+ {
+ CdbDispatchResults *pr = NULL;
+ CdbDispatcherState *ds = estate->dispatcherState;
+ DispatchWaitMode waitMode = DISPATCH_WAIT_NONE;
+ ErrorData *qeError = NULL;
+
+ /*
+ * If we are finishing a query before all the tuples of the query
+ * plan were fetched we must call ExecSquelchNode before checking
+ * the dispatch results in order to tell the nodes below we no longer
+ * need any more tuples.
+ */
+ if (!estate->es_got_eos)
+ {
+ ExecSquelchNode(queryDesc->planstate);
+ }
+
+ /*
+ * Wait for completion of all QEs. We send a "graceful" query
+ * finish, not cancel signal. Since the query has succeeded,
+ * don't confuse QEs by sending erroneous message.
+ */
+ if (estate->cancelUnfinished)
+ waitMode = DISPATCH_WAIT_FINISH;
+
+ cdbdisp_checkDispatchResult(ds, waitMode);
+
+ pr = cdbdisp_getDispatchResults(ds, &qeError);
+
+ if (qeError)
+ {
+ FlushErrorState();
+ ReThrowError(qeError);
+ }
+
+ if (ProcessDispatchResult_hook)
+ ProcessDispatchResult_hook(ds);
+
+ /* collect pgstat from QEs for current transaction level */
+ pgstat_combine_from_qe(pr, primaryWriterSliceIndex);
+
+ if (queryDesc->planstate->instrument && queryDesc->planstate->instrument->need_cdb)
+ {
+ cdbexplain_recvExecStats(queryDesc->planstate, ds->primaryResults,
+ LocallyExecutingSliceIndex(queryDesc->estate),
+ estate->showstatctx);
+ }
+ /* get num of rows processed from writer QEs. */
+ es_processed +=
+ cdbdisp_sumCmdTuples(pr, primaryWriterSliceIndex);
+
+ /* sum up rejected rows if any (single row error handling only) */
+ cdbdisp_sumRejectedRows(pr);
+
+ /*
+ * Check and free the results of all gangs. If any QE had an
+ * error, report it and exit to our error handler via PG_THROW.
+ * NB: This call doesn't wait, because we already waited above.
+ */
+ estate->dispatcherState = NULL;
+ cdbdisp_destroyDispatcherState(ds);
+ }
+
+ return es_processed;
+}
+
/*
* Cleanup the gp-specific parts of the query executor.
*
diff --git a/src/backend/executor/nodeModifyTable.c b/src/backend/executor/nodeModifyTable.c
index 3c2696639c7..7d98b69d345 100644
--- a/src/backend/executor/nodeModifyTable.c
+++ b/src/backend/executor/nodeModifyTable.c
@@ -2355,7 +2355,7 @@ fireBSTriggers(ModifyTableState *node)
/*
* Process AFTER EACH STATEMENT triggers
*/
-static void
+void
fireASTriggers(ModifyTableState *node)
{
ModifyTable *plan = (ModifyTable *) node->ps.plan;
@@ -3482,6 +3482,16 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags)
estate->es_auxmodifytables);
}
+ if (Gp_role == GP_ROLE_DISPATCH && !(eflags & EXEC_FLAG_EXPLAIN_ONLY))
+ {
+ estate->es_auxmodifytables = lcons(mtstate,
+ estate->es_auxmodifytables);
+ if (mtstate->fireBSTriggers)
+ {
+ fireBSTriggers(mtstate);
+ mtstate->fireBSTriggers = false;
+ }
+ }
return mtstate;
}
diff --git a/src/backend/executor/nodeNamedtuplestorescan.c b/src/backend/executor/nodeNamedtuplestorescan.c
index c0d1069f598..675da7b242d 100644
--- a/src/backend/executor/nodeNamedtuplestorescan.c
+++ b/src/backend/executor/nodeNamedtuplestorescan.c
@@ -17,6 +17,7 @@
#include "executor/execdebug.h"
#include "executor/nodeNamedtuplestorescan.h"
+#include "executor/nodeShareInputScan.h"
#include "miscadmin.h"
#include "utils/queryenvironment.h"
@@ -33,6 +34,8 @@ NamedTuplestoreScanNext(NamedTuplestoreScanState *node)
{
TupleTableSlot *slot;
+ if (!node->relation)
+ return NULL;
/* We intentionally do not support backward scan. */
Assert(ScanDirectionIsForward(node->ss.ps.state->es_direction));
@@ -84,6 +87,7 @@ ExecInitNamedTuplestoreScan(NamedTuplestoreScan *node, EState *estate, int eflag
{
NamedTuplestoreScanState *scanstate;
EphemeralNamedRelation enr;
+ void *reldata = NULL;
/* check for unsupported flags */
Assert(!(eflags & (EXEC_FLAG_BACKWARD | EXEC_FLAG_MARK)));
@@ -106,19 +110,27 @@ ExecInitNamedTuplestoreScan(NamedTuplestoreScan *node, EState *estate, int eflag
if (!enr)
elog(ERROR, "executor could not find named tuplestore \"%s\"",
node->enrname);
+ /* reldata is NULL means tuplestore is persistent, need to be read. */
+ if (enr->reldata == NULL)
+ {
+ reldata = (void*)tuplestore_open_shared_noerror(get_shareinput_fileset(), node->enrname);
+ }
+ else
+ reldata = enr->reldata;
- Assert(enr->reldata);
- scanstate->relation = (Tuplestorestate *) enr->reldata;
scanstate->tupdesc = ENRMetadataGetTupDesc(&(enr->md));
- scanstate->readptr =
- tuplestore_alloc_read_pointer(scanstate->relation, EXEC_FLAG_REWIND);
-
- /*
- * The new read pointer copies its position from read pointer 0, which
- * could be anywhere, so explicitly rewind it.
- */
- tuplestore_select_read_pointer(scanstate->relation, scanstate->readptr);
- tuplestore_rescan(scanstate->relation);
+ scanstate->relation = (Tuplestorestate *) reldata;
+
+ if (scanstate->relation)
+ {
+ scanstate->readptr = tuplestore_alloc_read_pointer(scanstate->relation, EXEC_FLAG_REWIND);
+ /*
+ * The new read pointer copies its position from read pointer 0, which
+ * could be anywhere, so explicitly rewind it.
+ */
+ tuplestore_select_read_pointer(scanstate->relation, scanstate->readptr);
+ tuplestore_rescan(scanstate->relation);
+ }
/*
* XXX: Should we add a function to free that read pointer when done?
@@ -164,6 +176,7 @@ ExecInitNamedTuplestoreScan(NamedTuplestoreScan *node, EState *estate, int eflag
void
ExecEndNamedTuplestoreScan(NamedTuplestoreScanState *node)
{
+ Tuplestorestate *ts = node->relation;
/*
* Free exprcontext
*/
@@ -175,6 +188,11 @@ ExecEndNamedTuplestoreScan(NamedTuplestoreScanState *node)
if (node->ss.ps.ps_ResultTupleSlot)
ExecClearTuple(node->ss.ps.ps_ResultTupleSlot);
ExecClearTuple(node->ss.ss_ScanTupleSlot);
+
+ if (ts && !tuplestore_in_memory(ts))
+ {
+ tuplestore_end(ts);
+ }
}
/* ----------------------------------------------------------------
@@ -196,6 +214,8 @@ ExecReScanNamedTuplestoreScan(NamedTuplestoreScanState *node)
/*
* Rewind my own pointer.
*/
+ if (!tuplestorestate)
+ return;
tuplestore_select_read_pointer(tuplestorestate, node->readptr);
tuplestore_rescan(tuplestorestate);
}
diff --git a/src/backend/executor/tstoreReceiver.c b/src/backend/executor/tstoreReceiver.c
index 48afaa22e53..766b32152c6 100644
--- a/src/backend/executor/tstoreReceiver.c
+++ b/src/backend/executor/tstoreReceiver.c
@@ -27,7 +27,7 @@
#include "access/detoast.h"
#include "access/tupconvert.h"
#include "executor/tstoreReceiver.h"
-
+#include "executor/nodeShareInputScan.h"
typedef struct
{
@@ -283,3 +283,223 @@ SetTuplestoreDestReceiverParams(DestReceiver *self,
myState->target_tupdesc = target_tupdesc;
myState->map_failure_msg = map_failure_msg;
}
+
+/*
+ * PersistentTupleStoreReceiver
+ *
+ * This receiver stores tuples in a persistent manner, allowing the
+ * tuples to be retrieved later even after the transaction has been
+ * committed.
+ */
+typedef struct PersistentTupleStore
+{
+ DestReceiver pub; /* base class */
+ ResourceOwner owner; /* resource owner for this receiver */
+ MemoryContext cxt; /* context containing tstore */
+ Tuplestorestate *tstore; /* state for storing tuples */
+ SharedFileSet *fileset; /* shared fileset for storing tuples */
+ const char *filename; /* filename for storing tuples */
+ bool detoast; /* were we told to detoast? */
+ bool initfile; /* is this the first time to init file? */
+ Datum *outvalues; /* values array for result tuple */
+ Datum *tofree; /* temp values to be pfree'd */
+} PersistentTupleStore;
+
+
+static bool
+persistentTstoreReceiveSlot_notoast(TupleTableSlot *slot, DestReceiver *self)
+{
+ PersistentTupleStore *myState = (PersistentTupleStore *) self;
+
+ if (!myState->initfile)
+ {
+ /* init file */
+ Assert(myState->fileset);
+ Assert(myState->filename);
+ tuplestore_make_sharedV2(myState->tstore, myState->fileset, myState->filename, myState->owner);
+ myState->initfile = true;
+ }
+ tuplestore_puttupleslot(myState->tstore, slot);
+
+ return true;
+}
+
+static bool
+persistentTstoreReceiveSlot_detoast(TupleTableSlot *slot, DestReceiver *self)
+{
+ PersistentTupleStore *myState = (PersistentTupleStore *) self;
+ TupleDesc typeinfo = slot->tts_tupleDescriptor;
+ int natts = typeinfo->natts;
+ int nfree;
+ int i;
+ MemoryContext oldcxt;
+
+ if (!myState->initfile)
+ {
+ /* init file */
+ Assert(myState->fileset);
+ Assert(myState->filename);
+ tuplestore_make_sharedV2(myState->tstore, myState->fileset, myState->filename, myState->owner);
+ myState->initfile = true;
+ }
+
+ /* Make sure the tuple is fully deconstructed */
+ slot_getallattrs(slot);
+
+ /*
+ * Fetch back any out-of-line datums. We build the new datums array in
+ * myState->outvalues[] (but we can re-use the slot's isnull array). Also,
+ * remember the fetched values to free afterwards.
+ */
+ nfree = 0;
+ for (i = 0; i < natts; i++)
+ {
+ Datum val = slot->tts_values[i];
+ Form_pg_attribute attr = TupleDescAttr(typeinfo, i);
+
+ if (!attr->attisdropped && attr->attlen == -1 && !slot->tts_isnull[i])
+ {
+ if (VARATT_IS_EXTERNAL(DatumGetPointer(val)))
+ {
+ val = PointerGetDatum(detoast_external_attr((struct varlena *)
+ DatumGetPointer(val)));
+ myState->tofree[nfree++] = val;
+ }
+ }
+
+ myState->outvalues[i] = val;
+ }
+
+ /*
+ * Push the modified tuple into the tuplestore.
+ */
+ oldcxt = MemoryContextSwitchTo(myState->cxt);
+ tuplestore_putvalues(myState->tstore, typeinfo,
+ myState->outvalues, slot->tts_isnull);
+ MemoryContextSwitchTo(oldcxt);
+
+ /* And release any temporary detoasted values */
+ for (i = 0; i < nfree; i++)
+ pfree(DatumGetPointer(myState->tofree[i]));
+
+ return true;
+}
+
+static void
+persistentTstoreStartupReceiver(DestReceiver *self, int operation, TupleDesc typeinfo)
+{
+ bool needtoast = false;
+ int natts = typeinfo->natts;
+ int i;
+ PersistentTupleStore *myState = (PersistentTupleStore *) self;
+ MemoryContext oldcxt;
+
+ oldcxt = MemoryContextSwitchTo(myState->cxt);
+ /* create in-memory tuplestore */
+ if (myState->tstore == NULL)
+ myState->tstore = tuplestore_begin_heap(true, false, work_mem);
+ MemoryContextSwitchTo(oldcxt);
+
+ if (myState->fileset == NULL)
+ myState->fileset = get_shareinput_fileset();
+
+ /* create tuplestore on disk */
+ Assert(myState->filename);
+ Assert(myState->owner);
+
+ /* Check if any columns require detoast work */
+ if (myState->detoast)
+ {
+ for (i = 0; i < natts; i++)
+ {
+ Form_pg_attribute attr = TupleDescAttr(typeinfo, i);
+
+ if (attr->attisdropped)
+ continue;
+ if (attr->attlen == -1)
+ {
+ needtoast = true;
+ break;
+ }
+ }
+ }
+
+ /* Set up appropriate callback */
+ if (needtoast)
+ {
+ myState->pub.receiveSlot = persistentTstoreReceiveSlot_detoast;
+ /* Create workspace */
+ myState->outvalues = (Datum *)
+ MemoryContextAlloc(myState->cxt, natts * sizeof(Datum));
+ myState->tofree = (Datum *)
+ MemoryContextAlloc(myState->cxt, natts * sizeof(Datum));
+ }
+ else
+ {
+ myState->pub.receiveSlot = persistentTstoreReceiveSlot_notoast;
+ myState->outvalues = NULL;
+ myState->tofree = NULL;
+ }
+
+}
+
+static void
+persistentTstoreShutdownReceiver(DestReceiver *self)
+{
+ PersistentTupleStore *myState = (PersistentTupleStore *) self;
+
+ if (myState->initfile)
+ {
+ /* Freeze tuplestore to file */
+ tuplestore_freeze(myState->tstore);
+
+ /* Set file to temporary to release file as soon as possible. */
+ tuplestore_set_flags(myState->tstore, true);
+ }
+ /* Release workspace if any */
+ if (myState->outvalues)
+ pfree(myState->outvalues);
+ myState->outvalues = NULL;
+ if (myState->tofree)
+ pfree(myState->tofree);
+ myState->tofree = NULL;
+}
+
+static void
+persistentTstoreDestroyReceiver(DestReceiver *self)
+{
+ pfree(self);
+}
+
+DestReceiver *
+CreatePersistentTstoreDestReceiver(void)
+{
+ PersistentTupleStore *self = palloc0(sizeof(PersistentTupleStore));
+
+ self->pub.receiveSlot = persistentTstoreReceiveSlot_notoast; /* might change */
+ self->pub.rStartup = persistentTstoreStartupReceiver;
+ self->pub.rShutdown = persistentTstoreShutdownReceiver;
+ self->pub.rDestroy = persistentTstoreDestroyReceiver;
+ self->pub.mydest = DestPersistentstore;
+
+ return (DestReceiver *) self;
+}
+
+void
+SetPersistentTstoreDestReceiverParams(DestReceiver *self,
+ Tuplestorestate *tStore,
+ ResourceOwner owner,
+ MemoryContext tContext,
+ bool detoast,
+ const char *filename)
+{
+ PersistentTupleStore *myState = (PersistentTupleStore *) self;
+
+ Assert(myState->pub.mydest == DestPersistentstore);
+ myState->tstore = tStore;
+ myState->owner = owner;
+ myState->cxt = tContext;
+ myState->detoast = detoast;
+ myState->filename = filename;
+ myState->initfile = false;
+}
diff --git a/src/backend/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c
index fc5cc237938..0da19116743 100644
--- a/src/backend/nodes/copyfuncs.c
+++ b/src/backend/nodes/copyfuncs.c
@@ -168,6 +168,11 @@ _copyQueryDispatchDesc(const QueryDispatchDesc *from)
COPY_SCALAR_FIELD(useChangedAOOpts);
COPY_SCALAR_FIELD(secContext);
COPY_NODE_FIELD(paramInfo);
+ COPY_NODE_FIELD(namedRelList);
+ COPY_SCALAR_FIELD(matviewOid);
+ COPY_SCALAR_FIELD(tableid);
+ COPY_SCALAR_FIELD(snaplen);
+ COPY_STRING_FIELD(snapname);
return newnode;
}
@@ -1800,6 +1805,9 @@ _copyIntoClause(const IntoClause *from)
COPY_NODE_FIELD(viewQuery);
COPY_SCALAR_FIELD(skipData);
COPY_NODE_FIELD(distributedBy);
+ COPY_SCALAR_FIELD(ivm);
+ COPY_SCALAR_FIELD(matviewOid);
+ COPY_STRING_FIELD(enrname);
return newnode;
}
@@ -2965,6 +2973,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);
@@ -5103,6 +5112,7 @@ _copyCreateTrigStmt(const CreateTrigStmt *from)
COPY_SCALAR_FIELD(deferrable);
COPY_SCALAR_FIELD(initdeferred);
COPY_NODE_FIELD(constrrel);
+ COPY_SCALAR_FIELD(matviewId);
return newnode;
}
@@ -6016,6 +6026,22 @@ _copyAlteredTableInfo(const AlteredTableInfo *from)
return newnode;
}
+static EphemeralNamedRelationInfo*
+_copyEphemeralNamedRelationInfo(const EphemeralNamedRelationInfo *from)
+{
+ EphemeralNamedRelationInfo *newnode = makeNode(EphemeralNamedRelationInfo);
+
+ COPY_STRING_FIELD(name);
+ COPY_SCALAR_FIELD(reliddesc);
+ COPY_SCALAR_FIELD(natts);
+
+ newnode->tuple = CreateTupleDescCopyConstr(from->tuple);
+ COPY_SCALAR_FIELD(enrtype);
+ COPY_SCALAR_FIELD(enrtuples);
+
+ return newnode;
+}
+
/*
* copyObjectImpl -- implementation of copyObject(); see nodes/nodes.h
*
@@ -7122,7 +7148,9 @@ copyObjectImpl(const void *from)
case T_AlteredTableInfo:
retval = _copyAlteredTableInfo(from);
break;
-
+ case T_EphemeralNamedRelationInfo:
+ retval = _copyEphemeralNamedRelationInfo(from);
+ break;
default:
elog(ERROR, "unrecognized node type: %d", (int) nodeTag(from));
retval = 0; /* keep compiler quiet */
diff --git a/src/backend/nodes/equalfuncs.c b/src/backend/nodes/equalfuncs.c
index 3bb25eb1d21..d95179fb339 100644
--- a/src/backend/nodes/equalfuncs.c
+++ b/src/backend/nodes/equalfuncs.c
@@ -173,7 +173,9 @@ _equalIntoClause(const IntoClause *a, const IntoClause *b)
COMPARE_NODE_FIELD(viewQuery);
COMPARE_SCALAR_FIELD(skipData);
COMPARE_NODE_FIELD(distributedBy);
-
+ COMPARE_SCALAR_FIELD(ivm);
+ COMPARE_SCALAR_FIELD(matviewOid);
+ COMPARE_STRING_FIELD(enrname);
return true;
}
@@ -2239,6 +2241,7 @@ _equalCreateTrigStmt(const CreateTrigStmt *a, const CreateTrigStmt *b)
COMPARE_SCALAR_FIELD(deferrable);
COMPARE_SCALAR_FIELD(initdeferred);
COMPARE_NODE_FIELD(constrrel);
+ COMPARE_SCALAR_FIELD(matviewId);
return true;
}
@@ -3008,6 +3011,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/outfast.c b/src/backend/nodes/outfast.c
index a8ab42e8a0d..102e8a30c90 100644
--- a/src/backend/nodes/outfast.c
+++ b/src/backend/nodes/outfast.c
@@ -1785,6 +1785,9 @@ _outNode(StringInfo str, void *obj)
case T_ReturnStmt:
_outReturnStmt(str, obj);
break;
+ case T_EphemeralNamedRelationInfo:
+ _outEphemeralNamedRelationInfo(str, obj);
+ break;
default:
elog(ERROR, "could not serialize unrecognized node type: %d",
(int) nodeTag(obj));
diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c
index 913d477630b..8900df8cd9c 100644
--- a/src/backend/nodes/outfuncs.c
+++ b/src/backend/nodes/outfuncs.c
@@ -1206,6 +1206,9 @@ _outIntoClause(StringInfo str, const IntoClause *node)
WRITE_NODE_FIELD(viewQuery);
WRITE_BOOL_FIELD(skipData);
WRITE_NODE_FIELD(distributedBy);
+ WRITE_BOOL_FIELD(ivm);
+ WRITE_OID_FIELD(matviewOid);
+ WRITE_STRING_FIELD(enrname);
}
static void
@@ -3496,6 +3499,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node)
WRITE_NODE_FIELD(alias);
WRITE_NODE_FIELD(eref);
WRITE_ENUM_FIELD(rtekind, RTEKind);
+ WRITE_BOOL_FIELD(relisivm);
switch (node->rtekind)
{
@@ -5150,7 +5154,9 @@ outNode(StringInfo str, const void *obj)
case T_GpPartitionListSpec:
_outGpPartitionListSpec(str, obj);
break;
-
+ case T_EphemeralNamedRelationInfo:
+ _outEphemeralNamedRelationInfo(str, obj);
+ break;
default:
/*
diff --git a/src/backend/nodes/outfuncs_common.c b/src/backend/nodes/outfuncs_common.c
index f777d6de2e7..4d42f6e21bf 100644
--- a/src/backend/nodes/outfuncs_common.c
+++ b/src/backend/nodes/outfuncs_common.c
@@ -244,6 +244,11 @@ _outQueryDispatchDesc(StringInfo str, const QueryDispatchDesc *node)
WRITE_STRING_FIELD(parallelCursorName);
WRITE_BOOL_FIELD(useChangedAOOpts);
WRITE_INT_FIELD(secContext);
+ WRITE_NODE_FIELD(namedRelList);
+ WRITE_OID_FIELD(matviewOid);
+ WRITE_OID_FIELD(tableid);
+ WRITE_INT_FIELD(snaplen);
+ WRITE_STRING_FIELD(snapname);
}
static void
@@ -1466,6 +1471,7 @@ _outCreateTrigStmt(StringInfo str, const CreateTrigStmt *node)
WRITE_BOOL_FIELD(deferrable);
WRITE_BOOL_FIELD(initdeferred);
WRITE_NODE_FIELD(constrrel);
+ WRITE_OID_FIELD(matviewId);
}
static void
@@ -1685,3 +1691,24 @@ _outTidRangeScan(StringInfo str, const TidRangeScan *node)
WRITE_NODE_FIELD(tidrangequals);
}
+
+static void
+_outEphemeralNamedRelationInfo(StringInfo str, const EphemeralNamedRelationInfo *node)
+{
+ int i;
+
+ WRITE_NODE_TYPE("EphemeralNamedRelationInfo");
+ WRITE_STRING_FIELD(name);
+ WRITE_OID_FIELD(reliddesc);
+ WRITE_INT_FIELD(natts);
+ WRITE_INT_FIELD(tuple->natts);
+
+ for (i = 0; i < node->tuple->natts; i++)
+ appendBinaryStringInfo(str, (char *) &node->tuple->attrs[i], ATTRIBUTE_FIXED_PART_SIZE);
+
+ WRITE_OID_FIELD(tuple->tdtypeid);
+ WRITE_INT_FIELD(tuple->tdtypmod);
+ WRITE_INT_FIELD(tuple->tdrefcount);
+ WRITE_ENUM_FIELD(enrtype, EphemeralNameRelationType);
+ WRITE_FLOAT_FIELD(enrtuples, "%.0f");
+}
diff --git a/src/backend/nodes/readfast.c b/src/backend/nodes/readfast.c
index 4013350ac05..0faa0c40f1e 100644
--- a/src/backend/nodes/readfast.c
+++ b/src/backend/nodes/readfast.c
@@ -837,6 +837,11 @@ _readQueryDispatchDesc(void)
READ_STRING_FIELD(parallelCursorName);
READ_BOOL_FIELD(useChangedAOOpts);
READ_INT_FIELD(secContext);
+ READ_NODE_FIELD(namedRelList);
+ READ_OID_FIELD(matviewOid);
+ READ_OID_FIELD(tableid);
+ READ_INT_FIELD(snaplen);
+ READ_STRING_FIELD(snapname);
READ_DONE();
}
@@ -1098,6 +1103,7 @@ _readCreateTrigStmt(void)
READ_BOOL_FIELD(deferrable);
READ_BOOL_FIELD(initdeferred);
READ_NODE_FIELD(constrrel);
+ READ_OID_FIELD(matviewId);
READ_DONE();
}
@@ -1636,6 +1642,38 @@ _readCreateStatsStmt(void)
READ_DONE();
}
+static EphemeralNamedRelationInfo *
+_readEphemeralNamedRelationInfo(void)
+{
+ READ_LOCALS(EphemeralNamedRelationInfo);
+
+ READ_STRING_FIELD(name);
+ READ_OID_FIELD(reliddesc);
+ READ_INT_FIELD(natts);
+
+ local_node->tuple = CreateTemplateTupleDesc(local_node->natts);
+
+ READ_INT_FIELD(tuple->natts);
+ if (local_node->tuple->natts > 0)
+ {
+ int i = 0;
+ for (; i < local_node->tuple->natts; i++)
+ {
+ memcpy(&local_node->tuple->attrs[i], read_str_ptr, ATTRIBUTE_FIXED_PART_SIZE);
+ read_str_ptr+=ATTRIBUTE_FIXED_PART_SIZE;
+ }
+ }
+
+ READ_OID_FIELD(tuple->tdtypeid);
+ READ_INT_FIELD(tuple->tdtypmod);
+ READ_INT_FIELD(tuple->tdrefcount);
+
+ READ_ENUM_FIELD(enrtype, EphemeralNameRelationType);
+ READ_FLOAT_FIELD(enrtuples);
+
+ READ_DONE();
+}
+
static void *
readNodeBinary(void)
{
@@ -2630,6 +2668,9 @@ readNodeBinary(void)
case T_StatsElem:
return_value = _readStatsElem();
break;
+ case T_EphemeralNamedRelationInfo:
+ return_value = _readEphemeralNamedRelationInfo();
+ break;
default:
return_value = NULL; /* keep the compiler silent */
elog(ERROR, "could not deserialize unrecognized node type: %d",
diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c
index 2453b8c86d2..d82095aa802 100644
--- a/src/backend/nodes/readfuncs.c
+++ b/src/backend/nodes/readfuncs.c
@@ -566,6 +566,9 @@ _readIntoClause(void)
READ_NODE_FIELD(viewQuery);
READ_BOOL_FIELD(skipData);
READ_NODE_FIELD(distributedBy);
+ READ_BOOL_FIELD(ivm);
+ READ_OID_FIELD(matviewOid);
+ READ_STRING_FIELD(enrname);
READ_DONE();
}
@@ -1440,6 +1443,7 @@ _readRangeTblEntry(void)
READ_NODE_FIELD(alias);
READ_NODE_FIELD(eref);
READ_ENUM_FIELD(rtekind, RTEKind);
+ READ_BOOL_FIELD(relisivm);
switch (local_node->rtekind)
{
diff --git a/src/backend/optimizer/path/allpaths.c b/src/backend/optimizer/path/allpaths.c
index 70d457e5780..16e4adf94e1 100644
--- a/src/backend/optimizer/path/allpaths.c
+++ b/src/backend/optimizer/path/allpaths.c
@@ -21,6 +21,7 @@
#include
#include
+#include "access/relation.h"
#include "access/sysattr.h"
#include "access/tsmapi.h"
#include "catalog/catalog.h"
@@ -2976,7 +2977,7 @@ set_cte_pathlist(PlannerInfo *root, RelOptInfo *rel, RangeTblEntry *rte)
config->honor_order_by = false;
- if (!cte->cterecursive)
+ if (!cte->cterecursive && subquery->commandType == CMD_SELECT)
{
/*
* Adjust the subquery so that 'root', i.e. this subquery, is the
@@ -3130,7 +3131,9 @@ set_namedtuplestore_pathlist(PlannerInfo *root, RelOptInfo *rel,
RangeTblEntry *rte)
{
Relids required_outer;
+ Relation relation = NULL;
+ relation = relation_open(rte->relid, NoLock);
/* Mark rel with estimated output rows, width, etc */
set_namedtuplestore_size_estimates(root, rel);
@@ -3140,10 +3143,14 @@ set_namedtuplestore_pathlist(PlannerInfo *root, RelOptInfo *rel,
* refs in its tlist.
*/
required_outer = rel->lateral_relids;
+ /* Use base table or matview's policy */
+ if (rel->cdbpolicy == NULL)
+ rel->cdbpolicy = relation->rd_cdbpolicy;
/* Generate appropriate path */
add_path(rel, create_namedtuplestorescan_path(root, rel, required_outer), root);
+ relation_close(relation, NoLock);
/* Select cheapest path (pretty easy in this case...) */
set_cheapest(rel);
}
diff --git a/src/backend/optimizer/util/pathnode.c b/src/backend/optimizer/util/pathnode.c
index 918c554579e..4536652ee61 100644
--- a/src/backend/optimizer/util/pathnode.c
+++ b/src/backend/optimizer/util/pathnode.c
@@ -3456,16 +3456,12 @@ create_namedtuplestorescan_path(PlannerInfo *root, RelOptInfo *rel,
pathnode->parallel_safe = rel->consider_parallel;
pathnode->parallel_workers = 0;
pathnode->pathkeys = NIL; /* result is always unordered */
-
- cost_namedtuplestorescan(pathnode, root, rel, pathnode->param_info);
-
/*
- * When this is used in triggers that run on QEs, the locus is ignored
- * and the scan is executed locally on the QE anyway. On QD, it's not
- * clear if named tuplestores are populated correctly in triggers, but if
- * it does work t all, Entry seems most appropriate.
+ * When this is used in triggers that run on QEs, the locus is should
+ * follow the distribution of base relation.
*/
- CdbPathLocus_MakeEntry(&pathnode->locus);
+ pathnode->locus = cdbpathlocus_from_baserel(root, rel, 0);
+ cost_namedtuplestorescan(pathnode, root, rel, pathnode->param_info);
return pathnode;
}
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index c9a49579eef..5bbbd36a79b 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -491,6 +491,7 @@ static void check_expressions_in_partition_key(PartitionSpec *spec, core_yyscan_
%type OptTempTableName
%type into_clause create_as_target create_mv_target
+%type incremental
%type createfunc_opt_item common_func_opt_item dostmt_opt_item
%type func_arg func_arg_with_default table_func_column aggr_arg
@@ -769,7 +770,7 @@ static void check_expressions_in_partition_key(PartitionSpec *spec, core_yyscan_
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
@@ -6839,31 +6840,33 @@ ext_opt_encoding_item:
*****************************************************************************/
CreateMatViewStmt:
- CREATE OptNoLog MATERIALIZED VIEW create_mv_target AS SelectStmt opt_with_data OptDistributedBy
+ CREATE OptNoLog incremental MATERIALIZED VIEW create_mv_target AS SelectStmt opt_with_data OptDistributedBy
{
CreateTableAsStmt *ctas = makeNode(CreateTableAsStmt);
- ctas->query = $7;
- ctas->into = $5;
+ ctas->query = $8;
+ ctas->into = $6;
ctas->objtype = 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);
- ctas->into->distributedBy = $9;
+ $6->rel->relpersistence = $2;
+ $6->skipData = !($9);
+ $6->ivm = $3;
+ ctas->into->distributedBy = $10;
$$ = (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->objtype = 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;
}
;
@@ -6880,11 +6883,16 @@ create_mv_target:
$$->tableSpaceName = $5;
$$->viewQuery = NULL; /* filled at analysis time */
$$->skipData = false; /* might get changed later */
+ $$->ivm = false;
$$->accessMethod = greenplumLegacyAOoptions($$->accessMethod, &$$->options);
}
;
+incremental: INCREMENTAL { $$ = true; }
+ | /*EMPTY*/ { $$ = false; }
+ ;
+
OptNoLog: UNLOGGED { $$ = RELPERSISTENCE_UNLOGGED; }
| /*EMPTY*/ { $$ = RELPERSISTENCE_PERMANENT; }
;
@@ -18794,6 +18802,7 @@ unreserved_keyword:
| INCLUDING
| INCLUSIVE
| INCREMENT
+ | INCREMENTAL
| INDEX
| INDEXES
| INHERIT
@@ -19730,6 +19739,7 @@ bare_label_keyword:
| INCLUDING
| INCLUSIVE
| INCREMENT
+ | INCREMENTAL
| INDEX
| INDEXES
| INHERIT
diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c
index 71118cf40a1..7001a143134 100644
--- a/src/backend/parser/parse_relation.c
+++ b/src/backend/parser/parse_relation.c
@@ -40,6 +40,7 @@
#include "utils/rel.h"
#include "utils/syscache.h"
#include "utils/varlena.h"
+#include "commands/matview.h"
#include "cdb/cdbvars.h"
@@ -85,7 +86,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref,
int count, int offset,
int rtindex, int sublevels_up,
int location, bool include_dropped,
- List **colnames, List **colvars);
+ List **colnames, List **colvars, bool is_ivm);
static int specialAttNum(const char *attname);
static bool isQueryUsingTempRelation_walker(Node *node, void *context);
@@ -1512,6 +1513,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
@@ -1601,6 +1603,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
@@ -2972,7 +2975,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up,
expandTupleDesc(tupdesc, rte->eref,
rtfunc->funccolcount, atts_done,
rtindex, sublevels_up, location,
- include_dropped, colnames, colvars);
+ include_dropped, colnames, colvars, false);
}
else if (functypclass == TYPEFUNC_SCALAR)
{
@@ -3240,7 +3243,7 @@ expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up,
expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0,
rtindex, sublevels_up,
location, include_dropped,
- colnames, colvars);
+ colnames, colvars, RelationIsIVM(rel));
relation_close(rel, AccessShareLock);
}
@@ -3257,7 +3260,7 @@ static void
expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset,
int rtindex, int sublevels_up,
int location, bool include_dropped,
- List **colnames, List **colvars)
+ List **colnames, List **colvars, bool is_ivm)
{
ListCell *aliascell;
int varattno;
@@ -3270,6 +3273,9 @@ expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset,
{
Form_pg_attribute attr = TupleDescAttr(tupdesc, varattno);
+ if (is_ivm && isIvmName(NameStr(attr->attname)) && !MatViewIncrementalMaintenanceIsEnabled())
+ continue;
+
if (attr->attisdropped)
{
if (include_dropped)
@@ -3423,6 +3429,10 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem,
Var *varnode = (Var *) lfirst(var);
TargetEntry *te;
+ /* if transform * into columnlist with IMMV, remove IVM columns */
+ if (rte->relisivm && isIvmName(label) && !MatViewIncrementalMaintenanceIsEnabled())
+ continue;
+
te = makeTargetEntry((Expr *) varnode,
(AttrNumber) pstate->p_next_resno++,
label,
diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c
index 5462848256f..6df8ffd2ead 100644
--- a/src/backend/rewrite/rewriteDefine.c
+++ b/src/backend/rewrite/rewriteDefine.c
@@ -822,7 +822,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/storage/file/buffile.c b/src/backend/storage/file/buffile.c
index b6f48dfaad0..b7e971f524b 100644
--- a/src/backend/storage/file/buffile.c
+++ b/src/backend/storage/file/buffile.c
@@ -440,6 +440,70 @@ BufFileOpenShared(SharedFileSet *fileset, const char *name, int mode)
return file;
}
+/*
+ * Open a shared BufFile with the given fileset, name, and mode.
+ *
+ * Parameters:
+ * - fileset: the shared fileset to open the BufFile in
+ * - name: the name of the BufFile
+ * - mode: the file mode to open the BufFile with
+ *
+ * Returns:
+ * - a pointer to the opened BufFile or maybe NULL no error.
+ */
+BufFile *
+BufFileOpenSharedV2(SharedFileSet *fileset, const char *name, int mode)
+{
+ BufFile *file = NULL;
+ char segment_name[MAXPGPATH];
+ Size capacity = 16;
+ File *files;
+ int nfiles = 0;
+
+ files = palloc(sizeof(File) * capacity);
+
+ /*
+ * We don't know how many segments there are, so we'll probe the
+ * filesystem to find out.
+ */
+ for (;;)
+ {
+ /* See if we need to expand our file segment array. */
+ if (nfiles + 1 > capacity)
+ {
+ capacity *= 2;
+ files = repalloc(files, sizeof(File) * capacity);
+ }
+ /* Try to load a segment. */
+ SharedSegmentName(segment_name, name, nfiles);
+ files[nfiles] = SharedFileSetOpen(fileset, segment_name, mode);
+ if (files[nfiles] <= 0)
+ break;
+ ++nfiles;
+
+ CHECK_FOR_INTERRUPTS();
+ }
+
+ /*
+ * If we didn't find any files at all, then no BufFile exists with this
+ * name.
+ */
+ if (nfiles == 0)
+ {
+ elog(DEBUG1, "could not find temporary file \"%s\" from BufFileOpenSharedV2 \"%s\": %m",
+ segment_name, name);
+ return file;
+ }
+
+ file = makeBufFileCommon(nfiles);
+ file->files = files;
+ file->readOnly = (mode == O_RDONLY) ? true : false;
+ file->fileset = fileset;
+ file->name = pstrdup(name);
+
+ return file;
+}
+
/*
* Delete a BufFile that was created by BufFileCreateShared in the given
* SharedFileSet using the given name.
@@ -1546,3 +1610,16 @@ BufFileTruncateShared(BufFile *file, int fileno, off_t offset)
}
/* Nothing to do, if the truncate point is beyond current file. */
}
+
+/*
+ * Set whether the BufFile is a temporary file or not.
+ */
+void
+BufFileSetIsTempFile(BufFile *file, bool isTempFile)
+{
+ int i;
+
+ /* close and delete the underlying file(s) */
+ for (i = 0; i < file->numFiles; i++)
+ FileSetTempfile(file->files[i], true);
+}
diff --git a/src/backend/storage/file/fd.c b/src/backend/storage/file/fd.c
index 670ead3e7f6..1e22fc6f616 100644
--- a/src/backend/storage/file/fd.c
+++ b/src/backend/storage/file/fd.c
@@ -3966,3 +3966,11 @@ pg_pwritev_with_retry(int fd, const struct iovec *iov, int iovcnt, off_t offset)
return sum;
}
+
+void FileSetTempfile(File file, bool isTemp)
+{
+ if (isTemp)
+ VfdCache[file].fdstate |= FD_DELETE_AT_CLOSE;
+ else
+ VfdCache[file].fdstate &= ~FD_DELETE_AT_CLOSE;
+}
diff --git a/src/backend/storage/ipc/ipci.c b/src/backend/storage/ipc/ipci.c
index c4dff0d3f02..fe3b4464cbb 100644
--- a/src/backend/storage/ipc/ipci.c
+++ b/src/backend/storage/ipc/ipci.c
@@ -30,6 +30,7 @@
#include "cdb/cdblocaldistribxact.h"
#include "cdb/cdbvars.h"
#include "commands/async.h"
+#include "commands/matview.h"
#include "crypto/kmgr.h"
#include "executor/nodeShareInputScan.h"
#include "miscadmin.h"
@@ -244,6 +245,7 @@ CreateSharedMemoryAndSemaphores(void)
/* size of standby promote flags */
size = add_size(size, ShmemStandbyPromoteReadySize());
#endif
+ size = add_size(size, mv_TableShmemSize());
elog(DEBUG3, "invoking IpcMemoryCreate(size=%zu)", size);
/*
diff --git a/src/backend/storage/lmgr/lwlocknames.txt b/src/backend/storage/lmgr/lwlocknames.txt
index 13d8980d1a3..a3496fc00c9 100644
--- a/src/backend/storage/lmgr/lwlocknames.txt
+++ b/src/backend/storage/lmgr/lwlocknames.txt
@@ -73,3 +73,4 @@ KmgrFileLock 63
GpParallelDSMHashLock 64
LoginFailedControlLock 65
LoginFailedSharedMemoryLock 66
+GPIVMResLock 67
diff --git a/src/backend/tcop/dest.c b/src/backend/tcop/dest.c
index 4f59391420b..deebbefa9ab 100644
--- a/src/backend/tcop/dest.c
+++ b/src/backend/tcop/dest.c
@@ -141,6 +141,8 @@ CreateDestReceiver(CommandDest dest)
case DestTuplestore:
return CreateTuplestoreDestReceiver();
+ case DestPersistentstore:
+ return CreatePersistentTstoreDestReceiver();
case DestIntoRel:
return CreateIntoRelDestReceiver(NULL);
@@ -211,6 +213,7 @@ EndCommand(const QueryCompletion *qc, CommandDest dest, bool force_undecorated_o
case DestSQLFunction:
case DestTransientRel:
case DestTupleQueue:
+ case DestPersistentstore:
break;
}
}
@@ -256,6 +259,7 @@ NullCommand(CommandDest dest)
case DestSQLFunction:
case DestTransientRel:
case DestTupleQueue:
+ case DestPersistentstore:
break;
}
}
@@ -310,6 +314,7 @@ ReadyForQuery(CommandDest dest)
case DestSQLFunction:
case DestTransientRel:
case DestTupleQueue:
+ case DestPersistentstore:
break;
}
}
diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index c545c42298c..c0f7b54a29f 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -110,6 +110,7 @@
#include "utils/session_state.h"
#include "utils/vmem_tracker.h"
#include "tcop/idle_resource_cleaner.h"
+#include "commands/matview.h"
/* ----------------
* global variables
@@ -1276,6 +1277,10 @@ exec_mpp_query(const char *query_string,
else
paramLI = NULL;
+ if (ddesc && ddesc->snaplen > 0)
+ {
+ AddPreassignedMVEntry(ddesc->matviewOid, ddesc->tableid, ddesc->snapname);
+ }
/*
* Switch back to transaction context to enter the loop.
*/
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 28808d63113..f8b5b15dc31 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -2130,6 +2130,17 @@ ProcessUtilitySlow(ParseState *pstate,
GetAssignedOidsForDispatch(),
NULL);
}
+ {
+ CreateTrigStmt *stmt = (CreateTrigStmt *) parsetree;
+ if (OidIsValid(stmt->matviewId))
+ {
+ ObjectAddress refaddr;
+ refaddr.classId = RelationRelationId;
+ refaddr.objectId = stmt->matviewId;
+ refaddr.objectSubId = 0;
+ recordDependencyOn(&address, &refaddr, DEPENDENCY_AUTO);
+ }
+ }
break;
case T_CreatePLangStmt:
diff --git a/src/backend/utils/cache/lsyscache.c b/src/backend/utils/cache/lsyscache.c
index fe6b8c56a56..6a1c2e1f7c2 100644
--- a/src/backend/utils/cache/lsyscache.c
+++ b/src/backend/utils/cache/lsyscache.c
@@ -2045,6 +2045,30 @@ is_agg_partial_capable(Oid aggid)
return result;
}
+/*
+ * 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 851ee051ce1..1faad8729cb 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -1971,6 +1971,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/backend/utils/init/postinit.c b/src/backend/utils/init/postinit.c
index f6980d14982..4b3168b533a 100644
--- a/src/backend/utils/init/postinit.c
+++ b/src/backend/utils/init/postinit.c
@@ -40,6 +40,7 @@
#include "catalog/pg_profile.h"
#include "commands/tablespace.h"
#include "datatype/timestamp.h"
+#include "commands/matview.h"
#include "libpq/auth.h"
#include "libpq/hba.h"
@@ -767,6 +768,7 @@ InitPostgres(const char *in_dbname, Oid dboid, const char *username,
/* Initialize portal manager */
EnablePortalManager();
+ mv_InitHashTables();
/* Initialize stats collection --- must happen before first xact */
if (!bootstrap)
diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c
index 5cc58b4f5d6..e8cc274756d 100644
--- a/src/backend/utils/misc/guc.c
+++ b/src/backend/utils/misc/guc.c
@@ -579,6 +579,7 @@ bool Debug_print_plan = false;
bool Debug_print_parse = false;
bool Debug_print_rewritten = false;
bool Debug_pretty_print = true;
+bool Debug_print_ivm = false;
bool log_parser_stats = false;
bool log_planner_stats = false;
@@ -1533,6 +1534,15 @@ static struct config_bool ConfigureNamesBool[] =
true,
NULL, NULL, NULL
},
+ {
+ {"debug_print_ivm", PGC_USERSET, LOGGING_WHAT,
+ gettext_noop("Logs incremental matview execution steps."),
+ NULL
+ },
+ &Debug_print_ivm,
+ false,
+ NULL, NULL, NULL
+ },
{
{"log_parser_stats", PGC_SUSET, STATS_MONITORING,
gettext_noop("Writes parser performance statistics to the server log."),
diff --git a/src/backend/utils/misc/queryenvironment.c b/src/backend/utils/misc/queryenvironment.c
index 86d61d083bf..7ba2b9c7bce 100644
--- a/src/backend/utils/misc/queryenvironment.c
+++ b/src/backend/utils/misc/queryenvironment.c
@@ -25,15 +25,21 @@
#include "access/table.h"
#include "utils/queryenvironment.h"
#include "utils/rel.h"
+#include "commands/matview.h"
/*
* Private state of a query environment.
*/
struct QueryEnvironment
{
- List *namedRelList;
+ List *namedRelList;
+ Size snaplen;
+ Oid matviewOid;
+ Oid tableid;
+ char *snapname;
};
+static List* construct_ENRList(QueryEnvironment *queryEnv);
QueryEnvironment *
create_queryEnv(void)
@@ -41,6 +47,108 @@ create_queryEnv(void)
return (QueryEnvironment *) palloc0(sizeof(QueryEnvironment));
}
+/*
+ * Configure the query environment with the given parameters.
+ */
+void
+configure_queryEnv(QueryEnvironment *queryEnv, Oid matviewOid, Oid tableid, char* snapname, Size snaplen)
+{
+ if (queryEnv == NULL)
+ return;
+ queryEnv->matviewOid = matviewOid;
+ queryEnv->tableid = tableid;
+ queryEnv->snaplen = snaplen;
+ queryEnv->snapname = snapname;
+}
+
+
+/*
+ * Construct a list of EphemeralNamedRelationInfo objects from the namedRelList
+ * in the given query environment.
+ *
+ * Parameters:
+ * - queryEnv: the query environment
+ *
+ * Returns:
+ * - a list of EphemeralNamedRelationInfo objects
+ */
+static List*
+construct_ENRList(QueryEnvironment *queryEnv)
+{
+ ListCell *lc;
+ List *enrList = NIL;
+ Assert(queryEnv);
+
+ foreach(lc, queryEnv->namedRelList)
+ {
+ EphemeralNamedRelation enr = (EphemeralNamedRelation) lfirst(lc);
+ EphemeralNamedRelationInfo *node = palloc0(sizeof(EphemeralNamedRelationInfo));
+ node->type = T_EphemeralNamedRelationInfo;
+ node->natts = enr->md.tupdesc->natts;
+ node->name = enr->md.name;
+ node->reliddesc = enr->md.reliddesc;
+ node->tuple = CreateTupleDescCopy(enr->md.tupdesc);
+ node->enrtype = enr->md.enrtype;
+ node->enrtuples = enr->md.enrtuples;
+ enrList = lappend(enrList, node);
+ }
+
+ return enrList;
+}
+
+/*
+ * Add preassigned EphemeralNamedRelationInfo objects to the given query environment.
+ *
+ * Parameters:
+ * - queryEnv: the query environment
+ * - enrs: a list of EphemeralNamedRelationInfo objects to add
+ */
+void
+AddPreassignedENR(QueryEnvironment *queryEnv, List* enrs)
+{
+ ListCell *lc;
+
+ foreach(lc, enrs)
+ {
+ EphemeralNamedRelationInfo *enrinfo = (EphemeralNamedRelationInfo *) lfirst(lc);
+
+ EphemeralNamedRelation enr = (EphemeralNamedRelation) palloc0(sizeof(EphemeralNamedRelationData));
+ enr->md.name = enrinfo->name;
+ enr->md.reliddesc = enrinfo->reliddesc;
+ enr->md.tupdesc = enrinfo->tuple;
+ enr->md.enrtype = enrinfo->enrtype;
+ enr->md.enrtuples = enrinfo->enrtuples;
+ enr->reldata = NULL;
+ if (get_visible_ENR_metadata(queryEnv, enr->md.name) == NULL)
+ register_ENR(queryEnv, enr);
+ else
+ pfree(enr);
+ }
+ return;
+}
+
+/*
+ * Fill the QueryDispatchDesc structure with information from the given query environment.
+ *
+ * Parameters:
+ * - queryEnv: the query environment
+ * - ddesc: the QueryDispatchDesc structure to fill
+ */
+void
+FillQueryDispatchDesc(QueryEnvironment *queryEnv, QueryDispatchDesc *ddesc)
+{
+ if (queryEnv == NULL)
+ return;
+ ddesc->namedRelList = construct_ENRList(queryEnv);
+ ddesc->snaplen = queryEnv->snaplen;
+ if (queryEnv->snaplen > 0)
+ {
+ ddesc->snapname = queryEnv->snapname;
+ ddesc->matviewOid = queryEnv->matviewOid;
+ ddesc->tableid = queryEnv->tableid;
+ }
+}
+
EphemeralNamedRelationMetadata
get_visible_ENR_metadata(QueryEnvironment *queryEnv, const char *refname)
{
@@ -126,8 +234,8 @@ ENRMetadataGetTupDesc(EphemeralNamedRelationMetadata enrmd)
{
TupleDesc tupdesc;
- /* One, and only one, of these fields must be filled. */
- Assert((enrmd->reliddesc == InvalidOid) != (enrmd->tupdesc == NULL));
+ /* not only one, but use reliddesc field first. */
+ Assert(enrmd->reliddesc || enrmd->tupdesc);
if (enrmd->tupdesc != NULL)
tupdesc = enrmd->tupdesc;
diff --git a/src/backend/utils/resowner/resowner.c b/src/backend/utils/resowner/resowner.c
index 39b93245005..d603263b3aa 100644
--- a/src/backend/utils/resowner/resowner.c
+++ b/src/backend/utils/resowner/resowner.c
@@ -1335,8 +1335,8 @@ ResourceOwnerForgetFile(ResourceOwner owner, File file)
static void
PrintFileLeakWarning(File file)
{
- elog(WARNING, "temporary file leak: File %d still referenced",
- file);
+ elog(WARNING, "temporary file %s leak: File %d still referenced",
+ FilePathName(file), file);
}
/*
diff --git a/src/backend/utils/sort/tuplestore.c b/src/backend/utils/sort/tuplestore.c
index 3903b450bcd..e21dadb76a5 100644
--- a/src/backend/utils/sort/tuplestore.c
+++ b/src/backend/utils/sort/tuplestore.c
@@ -83,6 +83,7 @@
#include "access/htup_details.h"
#include "commands/tablespace.h"
#include "executor/executor.h"
+#include "executor/nodeShareInputScan.h"
#include "miscadmin.h"
#include "storage/buffile.h"
#include "utils/memutils.h"
@@ -227,6 +228,8 @@ struct Tuplestorestate
struct Instrumentation *instrument;
long availMemMin; /* availMem low water mark (bytes) */
int64 spilledBytes; /* memory used for spilled tuples */
+
+ Oid tableid; /* table oid for ivm */
};
#define COPYTUP(state,tup) ((*(state)->copytup) (state, tup))
@@ -331,6 +334,7 @@ tuplestore_begin_common(int eflags, bool interXact, int maxKBytes)
state->memtupcount = 0;
state->tuples = 0;
state->remaining_tuples = 0;
+ state->tableid = InvalidOid;
/*
* Initial size of array must be more than ALLOCSET_SEPARATE_THRESHOLD;
@@ -906,29 +910,39 @@ tuplestore_puttuple_common(Tuplestorestate *state, void *tuple)
if (state->memtupcount < state->memtupsize && !LACKMEM(state))
return;
- /*
- * Nope; time to switch to tape-based operation. Make sure that
- * the temp file(s) are created in suitable temp tablespaces.
- */
- PrepareTempTablespaces();
+ if (OidIsValid(state->tableid) && state->shared_filename)
+ {
+ tuplestore_make_sharedV2(state,
+ get_shareinput_fileset(),
+ state->shared_filename,
+ tuplestore_get_resowner(state));
+ }
+ else
+ {
+ /*
+ * Nope; time to switch to tape-based operation. Make sure that
+ * the temp file(s) are created in suitable temp tablespaces.
+ */
+ PrepareTempTablespaces();
- /* associate the file with the store's resource owner */
- oldowner = CurrentResourceOwner;
- CurrentResourceOwner = state->resowner;
+ /* associate the file with the store's resource owner */
+ oldowner = CurrentResourceOwner;
+ CurrentResourceOwner = state->resowner;
- char tmpprefix[50];
- snprintf(tmpprefix, 50, "slice%d_tuplestore", currentSliceId);
- state->myfile = BufFileCreateTemp(tmpprefix, state->interXact);
+ char tmpprefix[50];
+ snprintf(tmpprefix, 50, "slice%d_tuplestore", currentSliceId);
+ state->myfile = BufFileCreateTemp(tmpprefix, state->interXact);
- CurrentResourceOwner = oldowner;
+ CurrentResourceOwner = oldowner;
- /*
- * Freeze the decision about whether trailing length words will be
- * used. We can't change this choice once data is on tape, even
- * though callers might drop the requirement.
- */
- state->backward = (state->eflags & EXEC_FLAG_BACKWARD) != 0;
- state->status = TSS_WRITEFILE;
+ /*
+ * Freeze the decision about whether trailing length words will be
+ * used. We can't change this choice once data is on tape, even
+ * though callers might drop the requirement.
+ */
+ state->backward = (state->eflags & EXEC_FLAG_BACKWARD) != 0;
+ state->status = TSS_WRITEFILE;
+ }
dumptuples(state);
break;
case TSS_WRITEFILE:
@@ -1807,3 +1821,176 @@ extern void tuplestore_consume_tuple(Tuplestorestate *state)
--state->remaining_tuples;
}
+/*
+ * tuplestore_open_shared_noerror
+ *
+ * Open a shared tuplestore that has been populated in another process
+ * for reading and ignore error when open an empty file.
+ */
+Tuplestorestate *
+tuplestore_open_shared_noerror(SharedFileSet *fileset, const char *filename)
+{
+ Tuplestorestate *state;
+ int eflags;
+ BufFile *myfile;
+
+ myfile = BufFileOpenSharedV2(fileset, filename, O_RDONLY);
+ if (myfile == NULL)
+ return NULL;
+
+ eflags = EXEC_FLAG_BACKWARD | EXEC_FLAG_REWIND;
+
+ state = tuplestore_begin_common(eflags,
+ false /* interXact, ignored because we open existing files */,
+ 10 /* no need for memory buffers */);
+
+ state->backward = true;
+
+ state->copytup = copytup_heap;
+ state->writetup = writetup_forbidden;
+ state->readtup = readtup_heap;
+
+ state->myfile = myfile;
+ state->readptrs[0].file = 0;
+ state->readptrs[0].offset = 0L;
+ state->status = TSS_READFILE;
+
+ state->share_status = TSHARE_READER;
+ state->frozen = false;
+ state->fileset = fileset;
+ state->shared_filename = pstrdup(filename);
+
+ return state;
+}
+
+/*
+ * tuplestore_make_sharedV2
+ *
+ * Make a tuplestore for sharing. This can be called
+ * after tuplestore_begin_heap().
+ */
+void
+tuplestore_make_sharedV2(Tuplestorestate *state, SharedFileSet *fileset,
+ const char *filename,
+ ResourceOwner owner)
+{
+ ResourceOwner oldowner;
+ MemoryContext oldctx;
+
+ if (state->status == TSS_WRITEFILE)
+ return;
+
+ oldctx = MemoryContextSwitchTo(TopMemoryContext);
+
+ state->work_set = workfile_mgr_create_set("SharedTupleStore", filename, true /* hold pin */);
+
+ Assert(state->status == TSS_INMEM);
+ Assert(state->share_status == TSHARE_NOT_SHARED);
+ state->share_status = TSHARE_WRITER;
+ state->fileset = fileset;
+ if (state->shared_filename == NULL)
+ state->shared_filename = pstrdup(filename);
+
+ /*
+ * Switch to tape-based operation, like in tuplestore_puttuple_common().
+ * We could delay this until tuplestore_freeze(), but we know we'll have
+ * to write everything to the file anyway, so let's not waste memory
+ * buffering the tuples in the meanwhile.
+ */
+ PrepareTempTablespaces();
+
+ /* associate the file with the store's resource owner */
+ oldowner = CurrentResourceOwner;
+ CurrentResourceOwner = owner;
+
+ state->myfile = BufFileCreateShared(fileset, filename, state->work_set);
+ CurrentResourceOwner = oldowner;
+
+ /*
+ * For now, be conservative and always use trailing length words for
+ * cross-process tuplestores. It's important that the writer and the
+ * reader processes agree on this, and forcing it to true is the
+ * simplest way to achieve that.
+ */
+ state->backward = true;
+ state->status = TSS_WRITEFILE;
+ MemoryContextSwitchTo(oldctx);
+}
+
+/*
+ * tuplestore_set_tuplecount
+ *
+ * Set the number of tuples in the tuplestore.
+ */
+void
+tuplestore_set_tuplecount(Tuplestorestate *state, int64 tuplecount)
+{
+ state->tuples = tuplecount;
+}
+
+/*
+ * tuplestore_get_sharedname
+ *
+ * Get the shared filename associated with the tuplestore.
+ */
+char *
+tuplestore_get_sharedname(Tuplestorestate *state)
+{
+ return state->shared_filename;
+}
+
+/*
+ * tuplestore_get_resowner
+ *
+ * Get the resource owner associated with the tuplestore.
+ */
+ResourceOwner
+tuplestore_get_resowner(Tuplestorestate *state)
+{
+ return state->resowner;
+}
+
+/*
+ * tuplestore_set_tableid
+ *
+ * Set the table OID associated with the tuplestore.
+ */
+void
+tuplestore_set_tableid(Tuplestorestate *state, Oid tableid)
+{
+ state->tableid = tableid;
+}
+
+/*
+ * tuplestore_set_sharedname
+ *
+ * Set the shared filename associated with the tuplestore.
+ */
+void
+tuplestore_set_sharedname(Tuplestorestate *state, char *sharedname)
+{
+ state->shared_filename = pstrdup(sharedname);
+}
+
+/*
+ * tuplestore_in_freezed
+ *
+ * Check if the tuplestore is frozen.
+ */
+bool
+tuplestore_in_freezed(Tuplestorestate *state)
+{
+ return state->frozen == true;
+}
+
+/*
+ * tuplestore_set_flags
+ *
+ * Set the flags for the tuplestore.
+ */
+void
+tuplestore_set_flags(Tuplestorestate *state, bool isTemp)
+{
+ /* Set the file as a temporary file */
+ BufFileSetIsTempFile(state->myfile, true);
+}
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 3749f5711db..a2f84cc0aae 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -7387,6 +7387,7 @@ getTables(Archive *fout, int *numTables)
int i_ispartition;
int i_partbound;
int i_amname;
+ int i_isivm;
/*
* Find all the tables and table-like objects.
@@ -7505,7 +7506,8 @@ getTables(Archive *fout, int *numTables)
"AS changed_acl, "
"%s AS partkeydef, "
"%s AS ispartition, "
- "%s AS partbound "
+ "%s AS partbound, "
+ "c.relisivm AS isivm "
"FROM pg_class c "
"LEFT JOIN pg_depend d ON "
"(c.relkind = '%c' AND "
@@ -8074,6 +8076,7 @@ getTables(Archive *fout, int *numTables)
i_ispartition = PQfnumber(res, "ispartition");
i_partbound = PQfnumber(res, "partbound");
i_amname = PQfnumber(res, "amname");
+ i_isivm = PQfnumber(res, "isivm");
if (dopt->lockWaitTimeout)
{
@@ -8200,6 +8203,7 @@ getTables(Archive *fout, int *numTables)
tblinfo[i].partkeydef = pg_strdup(PQgetvalue(res, i, i_partkeydef));
tblinfo[i].ispartition = (strcmp(PQgetvalue(res, i, i_ispartition), "t") == 0);
tblinfo[i].partbound = pg_strdup(PQgetvalue(res, i, i_partbound));
+ tblinfo[i].isivm = (strcmp(PQgetvalue(res, i, i_isivm), "t") == 0);
/* foreign server */
tblinfo[i].foreign_server = atooid(PQgetvalue(res, i, i_foreignserver));
@@ -18116,9 +18120,11 @@ dumpTableSchema(Archive *fout, const TableInfo *tbinfo)
}
}
- appendPQExpBuffer(q, "CREATE %s%s %s",
+ appendPQExpBuffer(q, "CREATE %s%s%s %s",
tbinfo->relpersistence == RELPERSISTENCE_UNLOGGED ?
"UNLOGGED " : "",
+ tbinfo->relkind == RELKIND_MATVIEW && tbinfo->isivm ?
+ "INCREMENTAL " : "",
reltypename,
qualrelname);
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index 60ed6c41f3e..3fa4d6448a0 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -396,6 +396,7 @@ typedef struct _tableInfo
struct _tableDataInfo *dataObj; /* TableDataInfo, if dumping its data */
int numTriggers; /* number of triggers for table */
struct _triggerInfo *triggers; /* array of TriggerInfo structs */
+ bool isivm; /* is incrementally maintainable materialized view? */
} TableInfo;
typedef struct _tableAttachInfo
diff --git a/src/bin/pg_dump/t/002_pg_dump.pl b/src/bin/pg_dump/t/002_pg_dump.pl
index 03d076806d6..a06a994ed3e 100644
--- a/src/bin/pg_dump/t/002_pg_dump.pl
+++ b/src/bin/pg_dump/t/002_pg_dump.pl
@@ -2171,6 +2171,21 @@
{ exclude_dump_test_schema => 1, no_toast_compression => 1, },
},
+ 'CREATE MATERIALIZED VIEW matview_ivm' => {
+ create_order => 21,
+ create_sql => 'CREATE INCREMENTAL MATERIALIZED VIEW dump_test.matview_ivm (col1) AS
+ SELECT col1 FROM dump_test.test_table;',
+ regexp => qr/^
+ \QCREATE INCREMENTAL MATERIALIZED VIEW dump_test.matview_ivm AS\E
+ \n\s+\QSELECT test_table.col1\E
+ \n\s+\QFROM dump_test.test_table\E
+ \n\s+\QWITH NO DATA;\E
+ /xm,
+ like =>
+ { %full_runs, %dump_test_schema_runs, section_pre_data => 1, },
+ unlike => { exclude_dump_test_schema => 1, },
+ },
+
'CREATE POLICY p1 ON test_table' => {
create_order => 22,
create_sql => 'CREATE POLICY p1 ON dump_test.test_table
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index ac531610557..9424da0358d 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -1923,6 +1923,7 @@ describeOneTableDetails(const char *schemaname,
char relpersistence;
char relreplident;
char *relam;
+ bool isivm;
char *compressionType;
char *compressionLevel;
@@ -1954,7 +1955,8 @@ describeOneTableDetails(const char *schemaname,
"c.relhastriggers, c.relrowsecurity, c.relforcerowsecurity, "
"false AS relhasoids, c.relispartition, %s, c.reltablespace, "
"CASE WHEN c.reloftype = 0 THEN '' ELSE c.reloftype::pg_catalog.regtype::pg_catalog.text END, "
- "c.relpersistence, c.relreplident, am.amname\n"
+ "c.relpersistence, c.relreplident, am.amname, "
+ "c.relisivm\n"
"FROM pg_catalog.pg_class c\n "
"LEFT JOIN pg_catalog.pg_class tc ON (c.reltoastrelid = tc.oid)\n"
"LEFT JOIN pg_catalog.pg_am am ON (c.relam = am.oid)\n"
@@ -2149,6 +2151,7 @@ describeOneTableDetails(const char *schemaname,
(char *) NULL : pg_strdup(PQgetvalue(res, 0, 14));
else
tableinfo.relam = NULL;
+ tableinfo.isivm = strcmp(PQgetvalue(res, 0, 15), "t") == 0;
/* GPDB Only: relstorage */
if (pset.sversion < 120000 && isGPDB())
@@ -4095,6 +4098,12 @@ describeOneTableDetails(const char *schemaname,
printfPQExpBuffer(&buf, _("Access method: %s"), tableinfo.relam);
printTableAddFooter(&cont, buf.data);
}
+
+ /* Incremental view maintance info */
+ if (verbose && tableinfo.relkind == RELKIND_MATVIEW && tableinfo.isivm)
+ {
+ printTableAddFooter(&cont, _("Incremental view maintenance: yes"));
+ }
}
/* reloptions, if verbose */
diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index b3a6a16ae17..0da375e667c 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -1106,6 +1106,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, THING_NO_DROP | THING_NO_ALTER},
{"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},
@@ -2777,7 +2778,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 (");
@@ -3107,13 +3108,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 with AS */
- else if (Matches("CREATE", "MATERIALIZED", "VIEW", MatchAny))
+ /* Complete CREATE MATERIALIZED VIEW with AS */
+ else if (Matches("CREATE", "MATERIALIZED", "VIEW", MatchAny) ||
+ Matches("CREATE", "INCREMENTAL", "MATERIALIZED", "VIEW", MatchAny))
COMPLETE_WITH("AS");
/* Complete "CREATE MATERIALIZED VIEW 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.h b/src/include/catalog/pg_class.h
index 02e56aed583..813d47fee83 100644
--- a/src/include/catalog/pg_class.h
+++ b/src/include/catalog/pg_class.h
@@ -119,6 +119,9 @@ CATALOG(pg_class,1259,RelationRelationId) BKI_BOOTSTRAP BKI_ROWTYPE_OID(83,Relat
/* is relation a partition? */
bool relispartition BKI_DEFAULT(f);
+ /* is relation a matview with ivm? */
+ bool relisivm BKI_DEFAULT(f);
+
/* link to original rel during table rewrite; otherwise 0 */
Oid relrewrite BKI_DEFAULT(0) BKI_LOOKUP_OPT(pg_class);
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 17f80fd5752..d0f32a6f433 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -12485,5 +12485,22 @@
{ oid => 7060, descr => 'get user account status',
proname => 'get_role_status', prorettype => 'cstring', proargtypes => 'cstring', prosrc => 'get_role_status' },
+# IVM
+{ oid => '786', descr => 'ivm trigger (before)',
+ proname => 'ivm_immediate_before', provolatile => 'v', prorettype => 'trigger',
+ proargtypes => '', prosrc => 'ivm_immediate_before' },
+{ oid => '787', descr => 'ivm trigger (after)',
+ proname => 'ivm_immediate_maintenance', provolatile => 'v', prorettype => 'trigger',
+ proargtypes => '', prosrc => 'ivm_immediate_maintenance' },
+{ oid => '788', descr => 'ivm filetring ',
+ proname => 'ivm_visible_in_prestate', provolatile => 's', prorettype => 'bool',
+ proargtypes => 'oid tid oid int4', prosrc => 'ivm_visible_in_prestate' },
+{ oid => '789', descr => 'ivm cleanup ',
+ proname => 'ivm_immediate_cleanup', provolatile => 'v', prorettype => 'bool',
+ proargtypes => 'oid', prosrc => 'ivm_immediate_cleanup', proexeclocation => 's'},
+{ oid => '7201', descr => 'export a snapshot for ivm',
+ proname => 'pg_export_snapshot_def', provolatile => 'v', proparallel => 'u',
+ prorettype => 'text', proargtypes => 'oid text',
+ prosrc => 'pg_export_snapshot_def', proexeclocation => 's' },
]
diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h
index e346df3c636..64cbccb3456 100644
--- a/src/include/commands/createas.h
+++ b/src/include/commands/createas.h
@@ -16,6 +16,7 @@
#include "catalog/objectaddress.h"
#include "nodes/params.h"
+#include "nodes/pathnodes.h"
#include "parser/parse_node.h"
#include "tcop/dest.h"
#include "utils/queryenvironment.h"
@@ -25,6 +26,12 @@ extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *st
ParamListInfo params, QueryEnvironment *queryEnv,
QueryCompletion *qc);
+extern void CreateIvmTriggersOnBaseTables(Query *qry, Oid matviewOid);
+extern void CreateIndexOnIMMV(Query *query, Relation matviewRel);
+
+extern Query *rewriteQueryForIMMV(Query *query, List *colNames);
+extern void makeIvmAggColumn(ParseState *pstate, Aggref *aggref, char *resname, AttrNumber *next_resno, List **aggs);
+
extern int GetIntoRelEFlags(IntoClause *intoClause);
extern DestReceiver *CreateIntoRelDestReceiver(IntoClause *intoClause);
diff --git a/src/include/commands/matview.h b/src/include/commands/matview.h
index 767232a6259..c509429baae 100644
--- a/src/include/commands/matview.h
+++ b/src/include/commands/matview.h
@@ -24,6 +24,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, QueryCompletion *qc);
@@ -33,5 +35,16 @@ extern DestReceiver *CreateTransientRelDestReceiver(Oid oid, Oid oldreloid, bool
extern bool MatViewIncrementalMaintenanceIsEnabled(void);
extern void transientrel_init(QueryDesc *queryDesc);
-
+extern void transientenr_init(QueryDesc *queryDesc);
+
+extern Datum ivm_immediate_before(PG_FUNCTION_ARGS);
+extern Datum ivm_immediate_maintenance(PG_FUNCTION_ARGS);
+extern Datum ivm_immediate_cleanup(PG_FUNCTION_ARGS);
+extern Datum ivm_visible_in_prestate(PG_FUNCTION_ARGS);
+extern void AtAbort_IVM(void);
+extern void AtEOXact_IVM(bool isCommit);
+extern bool isIvmName(const char *s);
+extern void mv_InitHashTables(void);
+extern Size mv_TableShmemSize(void);
+extern void AddPreassignedMVEntry(Oid matview_id, Oid table_id, const char* snapname);
#endif /* MATVIEW_H */
diff --git a/src/include/commands/trigger.h b/src/include/commands/trigger.h
index fc826b80006..832e229b88b 100644
--- a/src/include/commands/trigger.h
+++ b/src/include/commands/trigger.h
@@ -254,6 +254,10 @@ extern void AfterTriggerEndSubXact(bool isCommit);
extern void AfterTriggerSetState(ConstraintsSetStmt *stmt);
extern bool AfterTriggerPendingOnRel(Oid relid);
+extern void SetTransitionTablePreserved(Oid relid, CmdType cmdType);
+extern List* AfterTriggerGetMvList(void);
+extern void AfterTriggerAppendMvList(Oid matview_id);
+extern void SetTransitionTableName(Oid relid, CmdType cmdType, Oid mvoid);
/*
* in utils/adt/ri_triggers.c
diff --git a/src/include/executor/execdesc.h b/src/include/executor/execdesc.h
index d3800bea215..ad357d71d66 100644
--- a/src/include/executor/execdesc.h
+++ b/src/include/executor/execdesc.h
@@ -246,6 +246,14 @@ typedef struct QueryDispatchDesc
* Security context flags.
*/
int secContext;
+ /*
+ * Matview context fields
+ */
+ List *namedRelList;
+ Oid matviewOid;
+ Oid tableid;
+ int snaplen;
+ char *snapname;
} QueryDispatchDesc;
/*
@@ -336,5 +344,7 @@ extern QueryDesc *CreateQueryDesc(PlannedStmt *plannedstmt,
int instrument_options);
extern void FreeQueryDesc(QueryDesc *qdesc);
+extern void FillQueryDispatchDesc(QueryEnvironment *queryEnv, QueryDispatchDesc *ddesc);
+
#endif /* EXECDESC_H */
diff --git a/src/include/executor/executor.h b/src/include/executor/executor.h
index dc6b633fea3..5181b66746d 100644
--- a/src/include/executor/executor.h
+++ b/src/include/executor/executor.h
@@ -702,6 +702,8 @@ extern GpExecIdentity getGpExecIdentity(QueryDesc *queryDesc,
EState *estate);
extern void mppExecutorFinishup(QueryDesc *queryDesc);
extern void mppExecutorCleanup(QueryDesc *queryDesc);
+extern uint64 mppExecutorWait(QueryDesc *queryDesc);
+
extern ResultRelInfo *targetid_get_partition(Oid targetid, EState *estate, bool openIndices);
extern ResultRelInfo *slot_get_partition(TupleTableSlot *slot, EState *estate, bool openIndices);
diff --git a/src/include/executor/nodeModifyTable.h b/src/include/executor/nodeModifyTable.h
index 1bb21037714..b2b5f2109ec 100644
--- a/src/include/executor/nodeModifyTable.h
+++ b/src/include/executor/nodeModifyTable.h
@@ -24,4 +24,6 @@ extern void ExecEndModifyTable(ModifyTableState *node);
extern void ExecReScanModifyTable(ModifyTableState *node);
extern void ExecSquelchModifyTable(ModifyTableState *node);
+extern void fireASTriggers(ModifyTableState *node);
+
#endif /* NODEMODIFYTABLE_H */
diff --git a/src/include/executor/tstoreReceiver.h b/src/include/executor/tstoreReceiver.h
index 1f58b4f599e..3827400fb9d 100644
--- a/src/include/executor/tstoreReceiver.h
+++ b/src/include/executor/tstoreReceiver.h
@@ -20,12 +20,18 @@
extern DestReceiver *CreateTuplestoreDestReceiver(void);
-
+extern DestReceiver *CreatePersistentTstoreDestReceiver(void);
extern void SetTuplestoreDestReceiverParams(DestReceiver *self,
Tuplestorestate *tStore,
MemoryContext tContext,
bool detoast,
TupleDesc target_tupdesc,
const char *map_failure_msg);
+extern void SetPersistentTstoreDestReceiverParams(DestReceiver *self,
+ Tuplestorestate *tStore,
+ ResourceOwner owner,
+ MemoryContext ctx,
+ bool detoast,
+ const char *filename);
#endif /* TSTORE_RECEIVER_H */
diff --git a/src/include/nodes/nodes.h b/src/include/nodes/nodes.h
index e168fb85b73..371beaf89b7 100644
--- a/src/include/nodes/nodes.h
+++ b/src/include/nodes/nodes.h
@@ -659,6 +659,7 @@ typedef enum NodeTag
T_GpPolicy, /* in catalog/gp_distribution_policy.h */
T_RetrieveStmt,
T_ReindexIndexInfo, /* in nodes/parsenodes.h */
+ T_EphemeralNamedRelationInfo, /* utils/queryenvironment.h */
} NodeTag;
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index f2a7a520a03..d9a278ede94 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -1111,7 +1111,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; /* ivm relation or not */
/*
* Fields valid for a subquery RTE (else NULL):
*/
@@ -2994,6 +2994,7 @@ typedef struct CreateTrigStmt
bool deferrable; /* [NOT] DEFERRABLE */
bool initdeferred; /* INITIALLY {DEFERRED|IMMEDIATE} */
RangeVar *constrrel; /* opposite relation, if RI trigger */
+ Oid matviewId; /* matview oid */
} CreateTrigStmt;
/* ----------------------
diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h
index e555a746960..68b47e76695 100644
--- a/src/include/nodes/primnodes.h
+++ b/src/include/nodes/primnodes.h
@@ -122,6 +122,9 @@ typedef struct IntoClause
Node *viewQuery; /* materialized view's SELECT query */
bool skipData; /* true for WITH NO DATA */
Node *distributedBy; /* GPDB: columns to distribubte the data on. */
+ bool ivm; /* true for WITH IVM */
+ Oid matviewOid; /* matview oid */
+ char *enrname; /* ENR name for materialized view delta */
} IntoClause;
typedef struct CopyIntoClause
diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h
index a3fbbe72e33..d8aca8f4a43 100644
--- a/src/include/parser/kwlist.h
+++ b/src/include/parser/kwlist.h
@@ -232,6 +232,7 @@ PG_KEYWORD("include", INCLUDE, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("including", INCLUDING, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("inclusive", INCLUSIVE, UNRESERVED_KEYWORD, BARE_LABEL) /* GPDB */
PG_KEYWORD("increment", INCREMENT, UNRESERVED_KEYWORD, BARE_LABEL)
+PG_KEYWORD("incremental", INCREMENTAL, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("index", INDEX, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("indexes", INDEXES, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("inherit", INHERIT, UNRESERVED_KEYWORD, BARE_LABEL)
diff --git a/src/include/storage/buffile.h b/src/include/storage/buffile.h
index 15184acc1e7..b0e4cce9035 100644
--- a/src/include/storage/buffile.h
+++ b/src/include/storage/buffile.h
@@ -70,4 +70,5 @@ extern bool gp_workfile_compression;
extern void BufFilePledgeSequential(BufFile *buffile);
extern void BufFileSetIsTempFile(BufFile *file, bool isTempFile);
+extern BufFile *BufFileOpenSharedV2(SharedFileSet *fileset, const char *name, int mode);
#endif /* BUFFILE_H */
diff --git a/src/include/storage/fd.h b/src/include/storage/fd.h
index ef6c516b93a..3b0678b5d2f 100644
--- a/src/include/storage/fd.h
+++ b/src/include/storage/fd.h
@@ -189,4 +189,6 @@ extern const char *FileGetFilename(File file);
extern void FileSetIsWorkfile(File file);
+extern void FileSetTempfile(File file, bool isTemp);
+
#endif /* FD_H */
diff --git a/src/include/tcop/dest.h b/src/include/tcop/dest.h
index 36a18840616..8ed0c49b0b8 100644
--- a/src/include/tcop/dest.h
+++ b/src/include/tcop/dest.h
@@ -97,7 +97,8 @@ typedef enum
DestCopyOut, /* results sent to COPY TO code */
DestSQLFunction, /* results sent to SQL-language func mgr */
DestTransientRel, /* results sent to transient relation */
- DestTupleQueue /* results sent to tuple queue */
+ DestTupleQueue, /* results sent to tuple queue */
+ DestPersistentstore, /* results sent to Persistent Tuplestore */
} CommandDest;
/* ----------------
diff --git a/src/include/utils/guc.h b/src/include/utils/guc.h
index 4b43134c809..35a2154ce07 100644
--- a/src/include/utils/guc.h
+++ b/src/include/utils/guc.h
@@ -269,6 +269,7 @@ extern bool Debug_print_plan;
extern bool Debug_print_parse;
extern bool Debug_print_rewritten;
extern bool Debug_pretty_print;
+extern bool Debug_print_ivm;
extern bool Debug_print_full_dtm;
extern bool Debug_print_snapshot_dtm;
diff --git a/src/include/utils/lsyscache.h b/src/include/utils/lsyscache.h
index 6369d4d36fd..aa2d4014de6 100644
--- a/src/include/utils/lsyscache.h
+++ b/src/include/utils/lsyscache.h
@@ -171,6 +171,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/queryenvironment.h b/src/include/utils/queryenvironment.h
index 78f2f569530..da1eb81a5b9 100644
--- a/src/include/utils/queryenvironment.h
+++ b/src/include/utils/queryenvironment.h
@@ -55,6 +55,20 @@ typedef struct EphemeralNamedRelationData
typedef EphemeralNamedRelationData *EphemeralNamedRelation;
+
+typedef struct EphemeralNamedRelationInfo
+{
+ NodeTag type;
+ char *name; /* name used to identify the relation */
+ Oid reliddesc; /* oid of relation to get tupdesc */
+ int natts; /* # of attributes */
+ TupleDesc tuple; /* description of result rows */
+
+ EphemeralNameRelationType enrtype; /* to identify type of relation */
+ double enrtuples; /* number of tuples */
+} EphemeralNamedRelationInfo;
+
+
/*
* This is an opaque structure outside of queryenvironment.c itself. The
* intention is to be able to change the implementation or add new context
@@ -71,4 +85,6 @@ extern void unregister_ENR(QueryEnvironment *queryEnv, const char *name);
extern EphemeralNamedRelation get_ENR(QueryEnvironment *queryEnv, const char *name);
extern TupleDesc ENRMetadataGetTupDesc(EphemeralNamedRelationMetadata enrmd);
+extern void AddPreassignedENR(QueryEnvironment *queryEnv, List* enrs);
+extern void configure_queryEnv(QueryEnvironment *queryEnv, Oid matviewOid, Oid tableid, char* snapname, Size snaplen);
#endif /* QUERYENVIRONMENT_H */
diff --git a/src/include/utils/rel.h b/src/include/utils/rel.h
index 4b5c5ab0a25..3b2f4ab2922 100644
--- a/src/include/utils/rel.h
+++ b/src/include/utils/rel.h
@@ -731,6 +731,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
diff --git a/src/include/utils/tuplestore.h b/src/include/utils/tuplestore.h
index a2a07f05a27..0aebe88ac65 100644
--- a/src/include/utils/tuplestore.h
+++ b/src/include/utils/tuplestore.h
@@ -35,6 +35,7 @@
#include "executor/tuptable.h"
#include "storage/sharedfileset.h"
+#include "utils/resowner.h"
struct Instrumentation; /* #include "executor/instrument.h" */
@@ -106,4 +107,18 @@ extern bool tuplestore_has_remaining_tuples(Tuplestorestate *state);
extern void tuplestore_consume_tuple(Tuplestorestate *state);
+/*
+ * These routines only for IVM.
+*/
+extern Tuplestorestate *tuplestore_open_shared_noerror(SharedFileSet *fileset, const char *filename);
+extern bool tuplestore_in_freezed(Tuplestorestate *state);
+extern void tuplestore_set_flags(Tuplestorestate *state, bool isTemp);
+extern void tuplestore_make_sharedV2(Tuplestorestate *state, SharedFileSet *fileset,
+ const char *filename,
+ ResourceOwner owner);
+extern void tuplestore_set_tuplecount(Tuplestorestate *state, int64 tuplecount);
+extern char *tuplestore_get_sharedname(Tuplestorestate *state);
+extern ResourceOwner tuplestore_get_resowner(Tuplestorestate *state);
+extern void tuplestore_set_tableid(Tuplestorestate *state, Oid tableid);
+extern void tuplestore_set_sharedname(Tuplestorestate *state, char* sharedname);
#endif /* TUPLESTORE_H */
diff --git a/src/include/utils/unsync_guc_name.h b/src/include/utils/unsync_guc_name.h
index ffb4a8edaad..cd090e68c39 100644
--- a/src/include/utils/unsync_guc_name.h
+++ b/src/include/utils/unsync_guc_name.h
@@ -108,6 +108,7 @@
"debug_print_rewritten",
"debug_print_slice_table",
"debug_print_snapshot_dtm",
+ "debug_print_ivm",
"debug_resource_group",
"debug_walrepl_rcv",
"debug_walrepl_snd",
diff --git a/src/test/isolation2/output/parallel_retrieve_cursor/explain.source b/src/test/isolation2/output/parallel_retrieve_cursor/explain.source
index 4349887c935..2c638b56a22 100644
--- a/src/test/isolation2/output/parallel_retrieve_cursor/explain.source
+++ b/src/test/isolation2/output/parallel_retrieve_cursor/explain.source
@@ -94,50 +94,50 @@ EXPLAIN (VERBOSE, COSTS false) DECLARE c1 PARALLEL RETRIEVE CURSOR FOR SELECT *
-- Test for system table which is accessible on coordinator
EXPLAIN (VERBOSE, COSTS false) DECLARE c1 CURSOR FOR SELECT * FROM pg_class;
- QUERY PLAN
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
- Seq Scan on pg_catalog.pg_class
- Output: oid, relname, relnamespace, reltype, reloftype, relowner, relam, relfilenode, reltablespace, relpages, reltuples, relallvisible, reltoastrelid, relhasindex, relisshared, relpersistence, relkind, relnatts, relchecks, relhasrules, relhastriggers, relhassubclass, relrowsecurity, relforcerowsecurity, relispopulated, relreplident, relispartition, relrewrite, relfrozenxid, relminmxid, relacl, reloptions, relpartbound
- Optimizer: Postgres query optimizer
+ QUERY PLAN
+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Seq Scan on pg_catalog.pg_class
+ Output: oid, relname, relnamespace, reltype, reloftype, relowner, relam, relfilenode, reltablespace, relpages, reltuples, relallvisible, reltoastrelid, relhasindex, relisshared, relpersistence, relkind, relnatts, relchecks, relhasrules, relhastriggers, relhassubclass, relrowsecurity, relforcerowsecurity, relispopulated, relreplident, relispartition, relisivm, relrewrite, relfrozenxid, relminmxid, relacl, reloptions, relpartbound
+ Optimizer: Postgres query optimizer
(3 rows)
-- Test: explain output: Endpoint info (on coordinator/on some segments/on all segments)
EXPLAIN (VERBOSE, COSTS false) DECLARE c1 PARALLEL RETRIEVE CURSOR FOR SELECT * FROM pg_class;
- QUERY PLAN
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
- Seq Scan on pg_catalog.pg_class
- Output: oid, relname, relnamespace, reltype, reloftype, relowner, relam, relfilenode, reltablespace, relpages, reltuples, relallvisible, reltoastrelid, relhasindex, relisshared, relpersistence, relkind, relnatts, relchecks, relhasrules, relhastriggers, relhassubclass, relrowsecurity, relforcerowsecurity, relispopulated, relreplident, relispartition, relrewrite, relfrozenxid, relminmxid, relacl, reloptions, relpartbound
- Endpoint: "on coordinator"
- Optimizer: Postgres query optimizer
+ QUERY PLAN
+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Seq Scan on pg_catalog.pg_class
+ Output: oid, relname, relnamespace, reltype, reloftype, relowner, relam, relfilenode, reltablespace, relpages, reltuples, relallvisible, reltoastrelid, relhasindex, relisshared, relpersistence, relkind, relnatts, relchecks, relhasrules, relhastriggers, relhassubclass, relrowsecurity, relforcerowsecurity, relispopulated, relreplident, relispartition, relisivm, relrewrite, relfrozenxid, relminmxid, relacl, reloptions, relpartbound
+ Endpoint: "on coordinator"
+ Optimizer: Postgres query optimizer
(4 rows)
EXPLAIN (VERBOSE, COSTS false) DECLARE c1 PARALLEL RETRIEVE CURSOR FOR SELECT * FROM pg_class ORDER BY relname;
- QUERY PLAN
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
- Sort
- Output: oid, relname, relnamespace, reltype, reloftype, relowner, relam, relfilenode, reltablespace, relpages, reltuples, relallvisible, reltoastrelid, relhasindex, relisshared, relpersistence, relkind, relnatts, relchecks, relhasrules, relhastriggers, relhassubclass, relrowsecurity, relforcerowsecurity, relispopulated, relreplident, relispartition, relrewrite, relfrozenxid, relminmxid, relacl, reloptions, relpartbound
- Sort Key: pg_class.relname
- -> Seq Scan on pg_catalog.pg_class
- Output: oid, relname, relnamespace, reltype, reloftype, relowner, relam, relfilenode, reltablespace, relpages, reltuples, relallvisible, reltoastrelid, relhasindex, relisshared, relpersistence, relkind, relnatts, relchecks, relhasrules, relhastriggers, relhassubclass, relrowsecurity, relforcerowsecurity, relispopulated, relreplident, relispartition, relrewrite, relfrozenxid, relminmxid, relacl, reloptions, relpartbound
- Endpoint: "on coordinator"
- Optimizer: Postgres query optimizer
+ QUERY PLAN
+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Sort
+ Output: oid, relname, relnamespace, reltype, reloftype, relowner, relam, relfilenode, reltablespace, relpages, reltuples, relallvisible, reltoastrelid, relhasindex, relisshared, relpersistence, relkind, relnatts, relchecks, relhasrules, relhastriggers, relhassubclass, relrowsecurity, relforcerowsecurity, relispopulated, relreplident, relispartition, relisivm, relrewrite, relfrozenxid, relminmxid, relacl, reloptions, relpartbound
+ Sort Key: pg_class.relname
+ -> Seq Scan on pg_catalog.pg_class
+ Output: oid, relname, relnamespace, reltype, reloftype, relowner, relam, relfilenode, reltablespace, relpages, reltuples, relallvisible, reltoastrelid, relhasindex, relisshared, relpersistence, relkind, relnatts, relchecks, relhasrules, relhastriggers, relhassubclass, relrowsecurity, relforcerowsecurity, relispopulated, relreplident, relispartition, relisivm, relrewrite, relfrozenxid, relminmxid, relacl, reloptions, relpartbound
+ Endpoint: "on coordinator"
+ Optimizer: Postgres query optimizer
(7 rows)
EXPLAIN (VERBOSE, COSTS false) DECLARE c1 PARALLEL RETRIEVE CURSOR FOR SELECT * FROM pg_class WHERE gp_segment_id=1;
- QUERY PLAN
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
- Seq Scan on pg_catalog.pg_class
- Output: oid, relname, relnamespace, reltype, reloftype, relowner, relam, relfilenode, reltablespace, relpages, reltuples, relallvisible, reltoastrelid, relhasindex, relisshared, relpersistence, relkind, relnatts, relchecks, relhasrules, relhastriggers, relhassubclass, relrowsecurity, relforcerowsecurity, relispopulated, relreplident, relispartition, relrewrite, relfrozenxid, relminmxid, relacl, reloptions, relpartbound
- Filter: (pg_class.gp_segment_id = 1)
- Endpoint: "on coordinator"
- Optimizer: Postgres query optimizer
+ QUERY PLAN
+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Seq Scan on pg_catalog.pg_class
+ Output: oid, relname, relnamespace, reltype, reloftype, relowner, relam, relfilenode, reltablespace, relpages, reltuples, relallvisible, reltoastrelid, relhasindex, relisshared, relpersistence, relkind, relnatts, relchecks, relhasrules, relhastriggers, relhassubclass, relrowsecurity, relforcerowsecurity, relispopulated, relreplident, relispartition, relisivm, relrewrite, relfrozenxid, relminmxid, relacl, reloptions, relpartbound
+ Filter: (pg_class.gp_segment_id = 1)
+ Endpoint: "on coordinator"
+ Optimizer: Postgres query optimizer
(5 rows)
EXPLAIN (VERBOSE, COSTS false) DECLARE c1 PARALLEL RETRIEVE CURSOR FOR SELECT * FROM pg_class WHERE gp_segment_id=1 OR gp_segment_id=2;
- QUERY PLAN
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
- Seq Scan on pg_catalog.pg_class
- Output: oid, relname, relnamespace, reltype, reloftype, relowner, relam, relfilenode, reltablespace, relpages, reltuples, relallvisible, reltoastrelid, relhasindex, relisshared, relpersistence, relkind, relnatts, relchecks, relhasrules, relhastriggers, relhassubclass, relrowsecurity, relforcerowsecurity, relispopulated, relreplident, relispartition, relrewrite, relfrozenxid, relminmxid, relacl, reloptions, relpartbound
- Filter: ((pg_class.gp_segment_id = 1) OR (pg_class.gp_segment_id = 2))
- Endpoint: "on coordinator"
- Optimizer: Postgres query optimizer
+ QUERY PLAN
+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Seq Scan on pg_catalog.pg_class
+ Output: oid, relname, relnamespace, reltype, reloftype, relowner, relam, relfilenode, reltablespace, relpages, reltuples, relallvisible, reltoastrelid, relhasindex, relisshared, relpersistence, relkind, relnatts, relchecks, relhasrules, relhastriggers, relhassubclass, relrowsecurity, relforcerowsecurity, relispopulated, relreplident, relispartition, relisivm, relrewrite, relfrozenxid, relminmxid, relacl, reloptions, relpartbound
+ Filter: ((pg_class.gp_segment_id = 1) OR (pg_class.gp_segment_id = 2))
+ Endpoint: "on coordinator"
+ Optimizer: Postgres query optimizer
(5 rows)
-- Test for UDF which can be executed on coordinator
diff --git a/src/test/regress/expected/incremental_matview.out b/src/test/regress/expected/incremental_matview.out
new file mode 100644
index 00000000000..2c5aa00007d
--- /dev/null
+++ b/src/test/regress/expected/incremental_matview.out
@@ -0,0 +1,953 @@
+-- create a table to use as a basis for views and materialized views in various combinations
+CREATE TABLE mv_base_a (i int, j int) DISTRIBUTED BY (i);
+INSERT INTO mv_base_a VALUES
+ (1,10),
+ (2,20),
+ (3,30),
+ (4,40),
+ (5,50);
+CREATE TABLE mv_base_b (i int, k int) DISTRIBUTED BY (i);
+INSERT INTO mv_base_b VALUES
+ (1,101),
+ (2,102),
+ (3,103),
+ (4,104);
+CREATE INCREMENTAL MATERIALIZED VIEW mv_ivm_1 AS SELECT i,j,k FROM mv_base_a a INNER JOIN mv_base_b b USING(i) WITH NO DATA;
+NOTICE: Table doesn't have 'DISTRIBUTED BY' clause -- Using column(s) named 'i' as the Cloudberry Database data distribution key for this table.
+HINT: The 'DISTRIBUTED BY' clause determines the distribution of data. Make sure column(s) chosen are the optimal data distribution key to minimize skew.
+SELECT * FROM mv_ivm_1 ORDER BY 1,2,3;
+ERROR: materialized view "mv_ivm_1" has not been populated
+HINT: Use the REFRESH MATERIALIZED VIEW command.
+REFRESH MATERIALIZED VIEW mv_ivm_1;
+SELECT * FROM mv_ivm_1 ORDER BY 1,2,3;
+ i | j | k
+---+----+-----
+ 1 | 10 | 101
+ 2 | 20 | 102
+ 3 | 30 | 103
+ 4 | 40 | 104
+(4 rows)
+
+-- REFRESH WITH NO DATA
+BEGIN;
+CREATE FUNCTION dummy_ivm_trigger_func() RETURNS TRIGGER AS $$
+ BEGIN
+ RETURN NULL;
+ END
+$$ language plpgsql;
+CREATE CONSTRAINT TRIGGER dummy_ivm_trigger AFTER INSERT
+ON mv_base_a FROM mv_ivm_1 FOR EACH ROW
+EXECUTE PROCEDURE dummy_ivm_trigger_func();
+SELECT COUNT(*)
+FROM pg_depend pd INNER JOIN pg_trigger pt ON pd.objid = pt.oid
+WHERE pd.classid = 'pg_trigger'::regclass AND pd.refobjid = 'mv_ivm_1'::regclass;
+ count
+-------
+ 17
+(1 row)
+
+REFRESH MATERIALIZED VIEW mv_ivm_1 WITH NO DATA;
+SELECT COUNT(*)
+FROM pg_depend pd INNER JOIN pg_trigger pt ON pd.objid = pt.oid
+WHERE pd.classid = 'pg_trigger'::regclass AND pd.refobjid = 'mv_ivm_1'::regclass;
+ count
+-------
+ 1
+(1 row)
+
+ROLLBACK;
+-- immediate maintenance
+BEGIN;
+INSERT INTO mv_base_b VALUES(5,105);
+SELECT * FROM mv_ivm_1 ORDER BY 1,2,3;
+ i | j | k
+---+----+-----
+ 1 | 10 | 101
+ 2 | 20 | 102
+ 3 | 30 | 103
+ 4 | 40 | 104
+ 5 | 50 | 105
+(5 rows)
+
+UPDATE mv_base_a SET j = 0 WHERE i = 1;
+SELECT * FROM mv_ivm_1 ORDER BY 1,2,3;
+ i | j | k
+---+----+-----
+ 1 | 0 | 101
+ 2 | 20 | 102
+ 3 | 30 | 103
+ 4 | 40 | 104
+ 5 | 50 | 105
+(5 rows)
+
+DELETE FROM mv_base_b WHERE (i,k) = (5,105);
+SELECT * FROM mv_ivm_1 ORDER BY 1,2,3;
+ i | j | k
+---+----+-----
+ 1 | 0 | 101
+ 2 | 20 | 102
+ 3 | 30 | 103
+ 4 | 40 | 104
+(4 rows)
+
+ROLLBACK;
+SELECT * FROM mv_ivm_1 ORDER BY 1,2,3;
+ i | j | k
+---+----+-----
+ 1 | 10 | 101
+ 2 | 20 | 102
+ 3 | 30 | 103
+ 4 | 40 | 104
+(4 rows)
+
+-- rename of IVM columns
+CREATE INCREMENTAL MATERIALIZED VIEW mv_ivm_rename AS SELECT DISTINCT * FROM mv_base_a;
+NOTICE: Table doesn't have 'DISTRIBUTED BY' clause -- Using column(s) named 'i' as the Cloudberry Database data distribution key for this table.
+HINT: The 'DISTRIBUTED BY' clause determines the distribution of data. Make sure column(s) chosen are the optimal data distribution key to minimize skew.
+ALTER MATERIALIZED VIEW mv_ivm_rename RENAME COLUMN __ivm_count__ TO xxx;
+ERROR: IVM column can not be renamed
+DROP MATERIALIZED VIEW mv_ivm_rename;
+-- unique index on IVM columns
+CREATE INCREMENTAL MATERIALIZED VIEW mv_ivm_unique AS SELECT DISTINCT * FROM mv_base_a;
+NOTICE: Table doesn't have 'DISTRIBUTED BY' clause -- Using column(s) named 'i' as the Cloudberry Database data distribution key for this table.
+HINT: The 'DISTRIBUTED BY' clause determines the distribution of data. Make sure column(s) chosen are the optimal data distribution key to minimize skew.
+CREATE UNIQUE INDEX ON mv_ivm_unique(__ivm_count__);
+ERROR: unique index creation on IVM columns is not supported
+CREATE UNIQUE INDEX ON mv_ivm_unique((__ivm_count__));
+ERROR: unique index creation on IVM columns is not supported
+CREATE UNIQUE INDEX ON mv_ivm_unique((__ivm_count__ + 1));
+ERROR: unique index creation on IVM columns is not supported
+DROP MATERIALIZED VIEW mv_ivm_unique;
+-- TRUNCATE a base table in join views
+BEGIN;
+TRUNCATE mv_base_a;
+SELECT * FROM mv_ivm_1;
+ i | j | k
+---+---+---
+(0 rows)
+
+ROLLBACK;
+BEGIN;
+TRUNCATE mv_base_b;
+SELECT * FROM mv_ivm_1;
+ i | j | k
+---+---+---
+(0 rows)
+
+ROLLBACK;
+-- some query syntax
+BEGIN;
+CREATE FUNCTION ivm_func() RETURNS int LANGUAGE 'sql'
+ AS 'SELECT 1' IMMUTABLE;
+CREATE INCREMENTAL MATERIALIZED VIEW mv_ivm_func AS SELECT * FROM ivm_func();
+NOTICE: Table doesn't have 'DISTRIBUTED BY' clause -- Using column(s) named 'ivm_func' as the Cloudberry Database data distribution key for this table.
+HINT: The 'DISTRIBUTED BY' clause determines the distribution of data. Make sure column(s) chosen are the optimal data distribution key to minimize skew.
+CREATE INCREMENTAL MATERIALIZED VIEW mv_ivm_no_tbl AS SELECT 1;
+NOTICE: Table doesn't have 'DISTRIBUTED BY' clause -- Using column(s) named '?column?' as the Cloudberry Database data distribution key for this table.
+HINT: The 'DISTRIBUTED BY' clause determines the distribution of data. Make sure column(s) chosen are the optimal data distribution key to minimize skew.
+ROLLBACK;
+-- result of materialized view have DISTINCT clause or the duplicate result.
+BEGIN;
+CREATE INCREMENTAL MATERIALIZED VIEW mv_ivm_duplicate AS SELECT j FROM mv_base_a;
+NOTICE: Table doesn't have 'DISTRIBUTED BY' clause -- Using column(s) named 'j' as the Cloudberry Database data distribution key for this table.
+HINT: The 'DISTRIBUTED BY' clause determines the distribution of data. Make sure column(s) chosen are the optimal data distribution key to minimize skew.
+CREATE INCREMENTAL MATERIALIZED VIEW mv_ivm_distinct AS SELECT DISTINCT j FROM mv_base_a;
+NOTICE: Table doesn't have 'DISTRIBUTED BY' clause -- Using column(s) named 'j' as the Cloudberry Database data distribution key for this table.
+HINT: The 'DISTRIBUTED BY' clause determines the distribution of data. Make sure column(s) chosen are the optimal data distribution key to minimize skew.
+INSERT INTO mv_base_a VALUES(6,20);
+SELECT * FROM mv_ivm_duplicate ORDER BY 1;
+ j
+----
+ 10
+ 20
+ 20
+ 30
+ 40
+ 50
+(6 rows)
+
+SELECT * FROM mv_ivm_distinct ORDER BY 1;
+ j
+----
+ 10
+ 20
+ 30
+ 40
+ 50
+(5 rows)
+
+DELETE FROM mv_base_a WHERE (i,j) = (2,20);
+SELECT * FROM mv_ivm_duplicate ORDER BY 1;
+ j
+----
+ 10
+ 20
+ 30
+ 40
+ 50
+(5 rows)
+
+SELECT * FROM mv_ivm_distinct ORDER BY 1;
+ j
+----
+ 10
+ 20
+ 30
+ 40
+ 50
+(5 rows)
+
+ROLLBACK;
+-- support SUM(), COUNT() and AVG() aggregate functions
+BEGIN;
+CREATE INCREMENTAL MATERIALIZED VIEW mv_ivm_agg AS SELECT i, SUM(j), COUNT(i), AVG(j) FROM mv_base_a GROUP BY i;
+NOTICE: Table doesn't have 'DISTRIBUTED BY' clause -- Using column(s) named 'i' as the Cloudberry Database data distribution key for this table.
+HINT: The 'DISTRIBUTED BY' clause determines the distribution of data. Make sure column(s) chosen are the optimal data distribution key to minimize skew.
+SELECT * FROM mv_ivm_agg ORDER BY 1,2,3,4;
+ i | sum | count | avg
+---+-----+-------+---------------------
+ 1 | 10 | 1 | 10.0000000000000000
+ 2 | 20 | 1 | 20.0000000000000000
+ 3 | 30 | 1 | 30.0000000000000000
+ 4 | 40 | 1 | 40.0000000000000000
+ 5 | 50 | 1 | 50.0000000000000000
+(5 rows)
+
+INSERT INTO mv_base_a VALUES(2,100);
+SELECT * FROM mv_ivm_agg ORDER BY 1,2,3,4;
+ i | sum | count | avg
+---+-----+-------+---------------------
+ 1 | 10 | 1 | 10.0000000000000000
+ 2 | 120 | 2 | 60.0000000000000000
+ 3 | 30 | 1 | 30.0000000000000000
+ 4 | 40 | 1 | 40.0000000000000000
+ 5 | 50 | 1 | 50.0000000000000000
+(5 rows)
+
+UPDATE mv_base_a SET j = 200 WHERE (i,j) = (2,100);
+SELECT * FROM mv_ivm_agg ORDER BY 1,2,3,4;
+ i | sum | count | avg
+---+-----+-------+----------------------
+ 1 | 10 | 1 | 10.0000000000000000
+ 2 | 220 | 2 | 110.0000000000000000
+ 3 | 30 | 1 | 30.0000000000000000
+ 4 | 40 | 1 | 40.0000000000000000
+ 5 | 50 | 1 | 50.0000000000000000
+(5 rows)
+
+DELETE FROM mv_base_a WHERE (i,j) = (2,200);
+SELECT * FROM mv_ivm_agg ORDER BY 1,2,3,4;
+ i | sum | count | avg
+---+-----+-------+---------------------
+ 1 | 10 | 1 | 10.0000000000000000
+ 2 | 20 | 1 | 20.0000000000000000
+ 3 | 30 | 1 | 30.0000000000000000
+ 4 | 40 | 1 | 40.0000000000000000
+ 5 | 50 | 1 | 50.0000000000000000
+(5 rows)
+
+ROLLBACK;
+-- support COUNT(*) aggregate function
+BEGIN;
+CREATE INCREMENTAL MATERIALIZED VIEW mv_ivm_agg AS SELECT i, SUM(j), COUNT(*) FROM mv_base_a GROUP BY i;
+NOTICE: Table doesn't have 'DISTRIBUTED BY' clause -- Using column(s) named 'i' as the Cloudberry Database data distribution key for this table.
+HINT: The 'DISTRIBUTED BY' clause determines the distribution of data. Make sure column(s) chosen are the optimal data distribution key to minimize skew.
+SELECT * FROM mv_ivm_agg ORDER BY 1,2,3;
+ i | sum | count
+---+-----+-------
+ 1 | 10 | 1
+ 2 | 20 | 1
+ 3 | 30 | 1
+ 4 | 40 | 1
+ 5 | 50 | 1
+(5 rows)
+
+INSERT INTO mv_base_a VALUES(2,100);
+SELECT * FROM mv_ivm_agg ORDER BY 1,2,3;
+ i | sum | count
+---+-----+-------
+ 1 | 10 | 1
+ 2 | 120 | 2
+ 3 | 30 | 1
+ 4 | 40 | 1
+ 5 | 50 | 1
+(5 rows)
+
+ROLLBACK;
+-- TRUNCATE a base table in aggregate views
+BEGIN;
+CREATE INCREMENTAL MATERIALIZED VIEW mv_ivm_agg AS SELECT i, SUM(j), COUNT(*) FROM mv_base_a GROUP BY i;
+NOTICE: Table doesn't have 'DISTRIBUTED BY' clause -- Using column(s) named 'i' as the Cloudberry Database data distribution key for this table.
+HINT: The 'DISTRIBUTED BY' clause determines the distribution of data. Make sure column(s) chosen are the optimal data distribution key to minimize skew.
+TRUNCATE mv_base_a;
+SELECT sum, count FROM mv_ivm_agg;
+ sum | count
+-----+-------
+(0 rows)
+
+SELECT i, SUM(j), COUNT(*) FROM mv_base_a GROUP BY i;
+ i | sum | count
+---+-----+-------
+(0 rows)
+
+ROLLBACK;
+-- support aggregate functions without GROUP clause
+BEGIN;
+CREATE INCREMENTAL MATERIALIZED VIEW mv_ivm_group AS SELECT SUM(j), COUNT(j), AVG(j) FROM mv_base_a;
+NOTICE: Table doesn't have 'DISTRIBUTED BY' clause -- Using column(s) named 'sum' as the Cloudberry Database data distribution key for this table.
+HINT: The 'DISTRIBUTED BY' clause determines the distribution of data. Make sure column(s) chosen are the optimal data distribution key to minimize skew.
+SELECT * FROM mv_ivm_group ORDER BY 1;
+ sum | count | avg
+-----+-------+---------------------
+ 150 | 5 | 30.0000000000000000
+(1 row)
+
+INSERT INTO mv_base_a VALUES(6,60);
+SELECT * FROM mv_ivm_group ORDER BY 1;
+ sum | count | avg
+-----+-------+---------------------
+ 210 | 6 | 35.0000000000000000
+(1 row)
+
+DELETE FROM mv_base_a;
+SELECT * FROM mv_ivm_group ORDER BY 1;
+ sum | count | avg
+-----+-------+-----
+ | 0 |
+(1 row)
+
+ROLLBACK;
+-- TRUNCATE a base table in aggregate views without GROUP clause
+BEGIN;
+CREATE INCREMENTAL MATERIALIZED VIEW mv_ivm_group AS SELECT SUM(j), COUNT(j), AVG(j) FROM mv_base_a;
+NOTICE: Table doesn't have 'DISTRIBUTED BY' clause -- Using column(s) named 'sum' as the Cloudberry Database data distribution key for this table.
+HINT: The 'DISTRIBUTED BY' clause determines the distribution of data. Make sure column(s) chosen are the optimal data distribution key to minimize skew.
+TRUNCATE mv_base_a;
+SELECT sum, count, avg FROM mv_ivm_group;
+ sum | count | avg
+-----+-------+-----
+ | 0 |
+(1 row)
+
+SELECT SUM(j), COUNT(j), AVG(j) FROM mv_base_a;
+ sum | count | avg
+-----+-------+-----
+ | 0 |
+(1 row)
+
+ROLLBACK;
+-- resolved issue: When use AVG() function and values is indivisible, result of AVG() is incorrect.
+BEGIN;
+CREATE INCREMENTAL MATERIALIZED VIEW mv_ivm_avg_bug AS SELECT i, SUM(j), COUNT(j), AVG(j) FROM mv_base_A GROUP BY i;
+NOTICE: Table doesn't have 'DISTRIBUTED BY' clause -- Using column(s) named 'i' as the Cloudberry Database data distribution key for this table.
+HINT: The 'DISTRIBUTED BY' clause determines the distribution of data. Make sure column(s) chosen are the optimal data distribution key to minimize skew.
+SELECT * FROM mv_ivm_avg_bug ORDER BY 1,2,3;
+ i | sum | count | avg
+---+-----+-------+---------------------
+ 1 | 10 | 1 | 10.0000000000000000
+ 2 | 20 | 1 | 20.0000000000000000
+ 3 | 30 | 1 | 30.0000000000000000
+ 4 | 40 | 1 | 40.0000000000000000
+ 5 | 50 | 1 | 50.0000000000000000
+(5 rows)
+
+INSERT INTO mv_base_a VALUES
+ (1,0),
+ (1,0),
+ (2,30),
+ (2,30);
+SELECT * FROM mv_ivm_avg_bug ORDER BY 1,2,3;
+ i | sum | count | avg
+---+-----+-------+---------------------
+ 1 | 10 | 3 | 3.3333333333333333
+ 2 | 80 | 3 | 26.6666666666666667
+ 3 | 30 | 1 | 30.0000000000000000
+ 4 | 40 | 1 | 40.0000000000000000
+ 5 | 50 | 1 | 50.0000000000000000
+(5 rows)
+
+DELETE FROM mv_base_a WHERE (i,j) = (1,0);
+DELETE FROM mv_base_a WHERE (i,j) = (2,30);
+SELECT * FROM mv_ivm_avg_bug ORDER BY 1,2,3;
+ i | sum | count | avg
+---+-----+-------+---------------------
+ 1 | 10 | 1 | 10.0000000000000000
+ 2 | 20 | 1 | 20.0000000000000000
+ 3 | 30 | 1 | 30.0000000000000000
+ 4 | 40 | 1 | 40.0000000000000000
+ 5 | 50 | 1 | 50.0000000000000000
+(5 rows)
+
+ROLLBACK;
+-- aggregate views with column names specified
+BEGIN;
+CREATE INCREMENTAL MATERIALIZED VIEW mv_ivm_agg(a) AS SELECT i, SUM(j) FROM mv_base_a GROUP BY i;
+NOTICE: Table doesn't have 'DISTRIBUTED BY' clause -- Using column(s) named 'i' as the Cloudberry Database data distribution key for this table.
+HINT: The 'DISTRIBUTED BY' clause determines the distribution of data. Make sure column(s) chosen are the optimal data distribution key to minimize skew.
+INSERT INTO mv_base_a VALUES (1,100), (2,200), (3,300);
+UPDATE mv_base_a SET j = 2000 WHERE (i,j) = (2,20);
+DELETE FROM mv_base_a WHERE (i,j) = (3,30);
+SELECT * FROM mv_ivm_agg ORDER BY 1,2;
+ a | sum
+---+------
+ 1 | 110
+ 2 | 2200
+ 3 | 300
+ 4 | 40
+ 5 | 50
+(5 rows)
+
+ROLLBACK;
+BEGIN;
+CREATE INCREMENTAL MATERIALIZED VIEW mv_ivm_agg(a,b) AS SELECT i, SUM(j) FROM mv_base_a GROUP BY i;
+NOTICE: Table doesn't have 'DISTRIBUTED BY' clause -- Using column(s) named 'i' as the Cloudberry Database data distribution key for this table.
+HINT: The 'DISTRIBUTED BY' clause determines the distribution of data. Make sure column(s) chosen are the optimal data distribution key to minimize skew.
+INSERT INTO mv_base_a VALUES (1,100), (2,200), (3,300);
+UPDATE mv_base_a SET j = 2000 WHERE (i,j) = (2,20);
+DELETE FROM mv_base_a WHERE (i,j) = (3,30);
+SELECT * FROM mv_ivm_agg ORDER BY 1,2;
+ a | b
+---+------
+ 1 | 110
+ 2 | 2200
+ 3 | 300
+ 4 | 40
+ 5 | 50
+(5 rows)
+
+ROLLBACK;
+BEGIN;
+CREATE INCREMENTAL MATERIALIZED VIEW mv_ivm_agg(a,b,c) AS SELECT i, SUM(j) FROM mv_base_a GROUP BY i;
+NOTICE: Table doesn't have 'DISTRIBUTED BY' clause -- Using column(s) named 'i' as the Cloudberry Database data distribution key for this table.
+HINT: The 'DISTRIBUTED BY' clause determines the distribution of data. Make sure column(s) chosen are the optimal data distribution key to minimize skew.
+ERROR: too many column names were specified
+ROLLBACK;
+-- support self join view and multiple change on the same table
+BEGIN;
+CREATE TABLE base_t (i int, v int) DISTRIBUTED BY (i);
+INSERT INTO base_t VALUES (1, 10), (2, 20), (3, 30);
+CREATE INCREMENTAL MATERIALIZED VIEW mv_self(v1, v2) AS
+ SELECT t1.v, t2.v FROM base_t AS t1 JOIN base_t AS t2 ON t1.i = t2.i;
+NOTICE: Table doesn't have 'DISTRIBUTED BY' clause -- Using column(s) named 'v' as the Cloudberry Database data distribution key for this table.
+HINT: The 'DISTRIBUTED BY' clause determines the distribution of data. Make sure column(s) chosen are the optimal data distribution key to minimize skew.
+SELECT * FROM mv_self ORDER BY v1;
+ v1 | v2
+----+----
+ 10 | 10
+ 20 | 20
+ 30 | 30
+(3 rows)
+
+INSERT INTO base_t VALUES (4,40);
+DELETE FROM base_t WHERE i = 1;
+UPDATE base_t SET v = v*10 WHERE i=2;
+SELECT * FROM mv_self ORDER BY v1;
+ v1 | v2
+-----+-----
+ 30 | 30
+ 40 | 40
+ 200 | 200
+(3 rows)
+
+--- with sub-transactions
+SAVEPOINT p1;
+INSERT INTO base_t VALUES (7,70);
+RELEASE SAVEPOINT p1;
+INSERT INTO base_t VALUES (7,77);
+SELECT * FROM mv_self ORDER BY v1, v2;
+ v1 | v2
+-----+-----
+ 30 | 30
+ 40 | 40
+ 70 | 70
+ 70 | 77
+ 77 | 70
+ 77 | 77
+ 200 | 200
+(7 rows)
+
+ROLLBACK;
+-- views including NULL
+BEGIN;
+CREATE TABLE base_t (i int, v int) DISTRIBUTED BY (i);
+INSERT INTO base_t VALUES (1,10),(2, NULL);
+CREATE INCREMENTAL MATERIALIZED VIEW mv AS SELECT * FROM base_t;
+NOTICE: Table doesn't have 'DISTRIBUTED BY' clause -- Using column(s) named 'i' as the Cloudberry Database data distribution key for this table.
+HINT: The 'DISTRIBUTED BY' clause determines the distribution of data. Make sure column(s) chosen are the optimal data distribution key to minimize skew.
+SELECT * FROM mv ORDER BY i;
+ i | v
+---+----
+ 1 | 10
+ 2 |
+(2 rows)
+
+UPDATE base_t SET v = 20 WHERE i = 2;
+SELECT * FROM mv ORDER BY i;
+ i | v
+---+----
+ 1 | 10
+ 2 | 20
+(2 rows)
+
+ROLLBACK;
+BEGIN;
+CREATE TABLE base_t (i int) DISTRIBUTED BY (i);
+CREATE INCREMENTAL MATERIALIZED VIEW mv AS SELECT * FROM base_t;
+NOTICE: Table doesn't have 'DISTRIBUTED BY' clause -- Using column(s) named 'i' as the Cloudberry Database data distribution key for this table.
+HINT: The 'DISTRIBUTED BY' clause determines the distribution of data. Make sure column(s) chosen are the optimal data distribution key to minimize skew.
+SELECT * FROM mv ORDER BY i;
+ i
+---
+(0 rows)
+
+INSERT INTO base_t VALUES (1),(NULL);
+SELECT * FROM mv ORDER BY i;
+ i
+---
+ 1
+
+(2 rows)
+
+ROLLBACK;
+BEGIN;
+CREATE TABLE base_t (i int, v int) DISTRIBUTED BY (i);
+INSERT INTO base_t VALUES (NULL, 1), (NULL, 2), (1, 10), (1, 20);
+CREATE INCREMENTAL MATERIALIZED VIEW mv AS SELECT i, sum(v) FROM base_t GROUP BY i;
+NOTICE: Table doesn't have 'DISTRIBUTED BY' clause -- Using column(s) named 'i' as the Cloudberry Database data distribution key for this table.
+HINT: The 'DISTRIBUTED BY' clause determines the distribution of data. Make sure column(s) chosen are the optimal data distribution key to minimize skew.
+SELECT * FROM mv ORDER BY i;
+ i | sum
+---+-----
+ 1 | 30
+ | 3
+(2 rows)
+
+UPDATE base_t SET v = v * 10;
+SELECT * FROM mv ORDER BY i;
+ i | sum
+---+-----
+ 1 | 300
+ | 30
+(2 rows)
+
+ROLLBACK;
+-- support joins
+BEGIN;
+CREATE TABLE base_r(i int) DISTRIBUTED BY (i);
+CREATE TABLE base_s (i int, j int) DISTRIBUTED BY (i);
+CREATE TABLE base_t (j int) DISTRIBUTED BY (j);
+INSERT INTO base_r VALUES (1), (2), (3), (3);
+INSERT INTO base_s VALUES (2,1), (2,2), (3,1), (4,1), (4,2);
+INSERT INTO base_t VALUES (2), (3), (3);
+CREATE FUNCTION is_match() RETURNS text AS $$
+DECLARE
+x text;
+BEGIN
+ EXECUTE
+ 'SELECT CASE WHEN count(*) = 0 THEN ''OK'' ELSE ''NG'' END FROM (
+ SELECT * FROM (SELECT * FROM mv EXCEPT ALL SELECT * FROM v) v1
+ UNION ALL
+ SELECT * FROM (SELECT * FROM v EXCEPT ALL SELECT * FROM mv) v2
+ ) v' INTO x;
+ RETURN x;
+END;
+$$ LANGUAGE plpgsql;
+-- 3-way join
+CREATE INCREMENTAL MATERIALIZED VIEW mv(r, si, sj, t) AS
+ SELECT r.i, s.i, s.j, t.j
+ FROM base_r AS r JOIN base_s AS s ON r.i=s.i JOIN base_t AS t ON s.j=t.j;
+NOTICE: Table doesn't have 'DISTRIBUTED BY' clause -- Using column(s) named 'i' as the Cloudberry Database data distribution key for this table.
+HINT: The 'DISTRIBUTED BY' clause determines the distribution of data. Make sure column(s) chosen are the optimal data distribution key to minimize skew.
+CREATE VIEW v(r, si, sj, t) AS
+ SELECT r.i, s.i, s.j, t.j
+ FROM base_r AS r JOIN base_s AS s ON r.i=s.i JOIN base_t AS t ON s.j=t.j;
+SELECT * FROM mv ORDER BY r, si, sj, t;
+ r | si | sj | t
+---+----+----+---
+ 2 | 2 | 2 | 2
+(1 row)
+
+SAVEPOINT p1;
+INSERT INTO base_r VALUES (1),(2),(3);
+SELECT * FROM mv ORDER BY r, si, sj, t;
+ r | si | sj | t
+---+----+----+---
+ 2 | 2 | 2 | 2
+ 2 | 2 | 2 | 2
+(2 rows)
+
+SELECT is_match();
+ is_match
+----------
+ OK
+(1 row)
+
+INSERT INTO base_r VALUES (4),(5);
+SELECT * FROM mv ORDER BY r, si, sj, t;
+ r | si | sj | t
+---+----+----+---
+ 2 | 2 | 2 | 2
+ 2 | 2 | 2 | 2
+ 4 | 4 | 2 | 2
+(3 rows)
+
+SELECT is_match();
+ is_match
+----------
+ OK
+(1 row)
+
+ROLLBACK TO p1;
+INSERT INTO base_s VALUES (1,3);
+SELECT * FROM mv ORDER BY r, si, sj, t;
+ r | si | sj | t
+---+----+----+---
+ 1 | 1 | 3 | 3
+ 1 | 1 | 3 | 3
+ 2 | 2 | 2 | 2
+(3 rows)
+
+SELECT is_match();
+ is_match
+----------
+ OK
+(1 row)
+
+INSERT INTO base_s VALUES (2,3);
+SELECT * FROM mv ORDER BY r, si, sj, t;
+ r | si | sj | t
+---+----+----+---
+ 1 | 1 | 3 | 3
+ 1 | 1 | 3 | 3
+ 2 | 2 | 2 | 2
+ 2 | 2 | 3 | 3
+ 2 | 2 | 3 | 3
+(5 rows)
+
+SELECT is_match();
+ is_match
+----------
+ OK
+(1 row)
+
+ROLLBACK TO p1;
+INSERT INTO base_t VALUES (1),(2);
+SELECT * FROM mv ORDER BY r, si, sj, t;
+ r | si | sj | t
+---+----+----+---
+ 2 | 2 | 1 | 1
+ 2 | 2 | 2 | 2
+ 2 | 2 | 2 | 2
+ 3 | 3 | 1 | 1
+ 3 | 3 | 1 | 1
+(5 rows)
+
+SELECT is_match();
+ is_match
+----------
+ OK
+(1 row)
+
+INSERT INTO base_t VALUES (3),(4);
+SELECT * FROM mv ORDER BY r, si, sj, t;
+ r | si | sj | t
+---+----+----+---
+ 2 | 2 | 1 | 1
+ 2 | 2 | 2 | 2
+ 2 | 2 | 2 | 2
+ 3 | 3 | 1 | 1
+ 3 | 3 | 1 | 1
+(5 rows)
+
+SELECT is_match();
+ is_match
+----------
+ OK
+(1 row)
+
+ROLLBACK TO p1;
+DELETE FROM base_r WHERE i=1;
+SELECT * FROM mv ORDER BY r, si, sj, t;
+ r | si | sj | t
+---+----+----+---
+ 2 | 2 | 2 | 2
+(1 row)
+
+SELECT is_match();
+ is_match
+----------
+ OK
+(1 row)
+
+DELETE FROM base_r WHERE i=2;
+SELECT * FROM mv ORDER BY r, si, sj, t;
+ r | si | sj | t
+---+----+----+---
+(0 rows)
+
+SELECT is_match();
+ is_match
+----------
+ OK
+(1 row)
+
+DELETE FROM base_r WHERE i=3;
+SELECT * FROM mv ORDER BY r, si, sj, t;
+ r | si | sj | t
+---+----+----+---
+(0 rows)
+
+SELECT is_match();
+ is_match
+----------
+ OK
+(1 row)
+
+ROLLBACK TO p1;
+DELETE FROM base_s WHERE i=2;
+SELECT * FROM mv ORDER BY r, si, sj, t;
+ r | si | sj | t
+---+----+----+---
+(0 rows)
+
+SELECT is_match();
+ is_match
+----------
+ OK
+(1 row)
+
+DELETE FROM base_s WHERE i=3;
+SELECT * FROM mv ORDER BY r, si, sj, t;
+ r | si | sj | t
+---+----+----+---
+(0 rows)
+
+SELECT is_match();
+ is_match
+----------
+ OK
+(1 row)
+
+DELETE FROM base_s WHERE i=4;
+SELECT * FROM mv ORDER BY r, si, sj, t;
+ r | si | sj | t
+---+----+----+---
+(0 rows)
+
+SELECT is_match();
+ is_match
+----------
+ OK
+(1 row)
+
+ROLLBACK TO p1;
+DELETE FROM base_t WHERE j=2;
+SELECT * FROM mv ORDER BY r, si, sj, t;
+ r | si | sj | t
+---+----+----+---
+(0 rows)
+
+SELECT is_match();
+ is_match
+----------
+ OK
+(1 row)
+
+DELETE FROM base_t WHERE j=3;
+SELECT * FROM mv ORDER BY r, si, sj, t;
+ r | si | sj | t
+---+----+----+---
+(0 rows)
+
+SELECT is_match();
+ is_match
+----------
+ OK
+(1 row)
+
+ROLLBACK TO p1;
+-- TRUNCATE a base table in views
+TRUNCATE base_r;
+SELECT is_match();
+ is_match
+----------
+ OK
+(1 row)
+
+ROLLBACK TO p1;
+TRUNCATE base_s;
+SELECT is_match();
+ is_match
+----------
+ OK
+(1 row)
+
+ROLLBACK TO p1;
+TRUNCATE base_t;
+SELECT is_match();
+ is_match
+----------
+ OK
+(1 row)
+
+ROLLBACK TO p1;
+DROP MATERIALIZED VIEW mv;
+DROP VIEW v;
+ROLLBACK;
+-- IMMV containing user defined type
+BEGIN;
+CREATE TYPE mytype;
+CREATE FUNCTION mytype_in(cstring)
+ RETURNS mytype AS 'int4in'
+ LANGUAGE INTERNAL STRICT IMMUTABLE;
+NOTICE: return type mytype is only a shell
+CREATE FUNCTION mytype_out(mytype)
+ RETURNS cstring AS 'int4out'
+ LANGUAGE INTERNAL STRICT IMMUTABLE;
+NOTICE: argument type mytype is only a shell
+CREATE TYPE mytype (
+ LIKE = int4,
+ INPUT = mytype_in,
+ OUTPUT = mytype_out
+);
+CREATE FUNCTION mytype_eq(mytype, mytype)
+ RETURNS bool AS 'int4eq'
+ LANGUAGE INTERNAL STRICT IMMUTABLE;
+CREATE FUNCTION mytype_lt(mytype, mytype)
+ RETURNS bool AS 'int4lt'
+ LANGUAGE INTERNAL STRICT IMMUTABLE;
+CREATE FUNCTION mytype_cmp(mytype, mytype)
+ RETURNS integer AS 'btint4cmp'
+ LANGUAGE INTERNAL STRICT IMMUTABLE;
+CREATE OPERATOR = (
+ leftarg = mytype, rightarg = mytype,
+ procedure = mytype_eq);
+CREATE OPERATOR < (
+ leftarg = mytype, rightarg = mytype,
+ procedure = mytype_lt);
+CREATE OPERATOR CLASS mytype_ops
+ DEFAULT FOR TYPE mytype USING btree AS
+ OPERATOR 1 <,
+ OPERATOR 3 = ,
+ FUNCTION 1 mytype_cmp(mytype,mytype);
+CREATE TABLE t_mytype (x mytype);
+NOTICE: Table doesn't have 'DISTRIBUTED BY' clause, and no column type is suitable for a distribution key. Creating a NULL policy entry.
+CREATE INCREMENTAL MATERIALIZED VIEW mv_mytype AS
+ SELECT * FROM t_mytype;
+NOTICE: Table doesn't have 'DISTRIBUTED BY' clause -- Using column(s) named '' as the Cloudberry Database data distribution key for this table.
+HINT: The 'DISTRIBUTED BY' clause determines the distribution of data. Make sure column(s) chosen are the optimal data distribution key to minimize skew.
+INSERT INTO t_mytype VALUES ('1'::mytype);
+SELECT * FROM mv_mytype;
+ x
+---
+ 1
+(1 row)
+
+ROLLBACK;
+-- outer join is not supported
+CREATE INCREMENTAL MATERIALIZED VIEW mv(a,b) AS SELECT a.i, b.i FROM mv_base_a a LEFT JOIN mv_base_b b ON a.i=b.i;
+ERROR: OUTER JOIN is not supported on incrementally maintainable materialized view
+-- CTE is not supported
+CREATE INCREMENTAL MATERIALIZED VIEW mv AS
+ WITH b AS ( SELECT * FROM mv_base_b) SELECT a.i,a.j FROM mv_base_a a, b WHERE a.i = b.i;
+ERROR: CTE is not supported on incrementally maintainable materialized view
+-- contain system column
+CREATE INCREMENTAL MATERIALIZED VIEW mv_ivm01 AS SELECT i,j,xmin FROM mv_base_a;
+ERROR: system column is not supported on incrementally maintainable materialized view
+CREATE INCREMENTAL MATERIALIZED VIEW mv_ivm02 AS SELECT i,j FROM mv_base_a WHERE xmin = '610';
+ERROR: system column is not supported on incrementally maintainable materialized view
+CREATE INCREMENTAL MATERIALIZED VIEW mv_ivm04 AS SELECT i,j,xmin::text AS x_min FROM mv_base_a;
+ERROR: system column is not supported on incrementally maintainable materialized view
+CREATE INCREMENTAL MATERIALIZED VIEW mv_ivm06 AS SELECT i,j,xidsend(xmin) AS x_min FROM mv_base_a;
+ERROR: system column is not supported on incrementally maintainable materialized view
+-- contain subquery
+CREATE INCREMENTAL MATERIALIZED VIEW mv_ivm03 AS SELECT i,j FROM mv_base_a WHERE i IN (SELECT i FROM mv_base_b WHERE k < 103 );
+ERROR: subquery is not supported on incrementally maintainable materialized view
+CREATE INCREMENTAL MATERIALIZED VIEW mv_ivm04 AS SELECT a.i,a.j FROM mv_base_a a, (SELECT * FROM mv_base_b) b WHERE a.i = b.i;
+ERROR: subquery is not supported on incrementally maintainable materialized view
+CREATE INCREMENTAL MATERIALIZED VIEW mv_ivm05 AS SELECT i,j, (SELECT k FROM mv_base_b b WHERE a.i = b.i) FROM mv_base_a a;
+ERROR: subquery is not supported on incrementally maintainable materialized view
+-- contain ORDER BY
+CREATE INCREMENTAL MATERIALIZED VIEW mv_ivm07 AS SELECT i,j,k FROM mv_base_a a INNER JOIN mv_base_b b USING(i) ORDER BY i,j,k;
+ERROR: ORDER BY clause is not supported on incrementally maintainable materialized view
+-- contain HAVING
+CREATE INCREMENTAL MATERIALIZED VIEW mv_ivm08 AS SELECT i,j,k FROM mv_base_a a INNER JOIN mv_base_b b USING(i) GROUP BY i,j,k HAVING SUM(i) > 5;
+ERROR: HAVING clause is not supported on incrementally maintainable materialized view
+-- contain view or materialized view
+CREATE VIEW b_view AS SELECT i,k FROM mv_base_b;
+CREATE MATERIALIZED VIEW b_mview AS SELECT i,k FROM mv_base_b;
+NOTICE: Table doesn't have 'DISTRIBUTED BY' clause -- Using column(s) named 'i' as the Cloudberry Database data distribution key for this table.
+HINT: The 'DISTRIBUTED BY' clause determines the distribution of data. Make sure column(s) chosen are the optimal data distribution key to minimize skew.
+CREATE INCREMENTAL MATERIALIZED VIEW mv_ivm07 AS SELECT a.i,a.j FROM mv_base_a a,b_view b WHERE a.i = b.i;
+ERROR: VIEW or MATERIALIZED VIEW is not supported on incrementally maintainable materialized view
+CREATE INCREMENTAL MATERIALIZED VIEW mv_ivm08 AS SELECT a.i,a.j FROM mv_base_a a,b_mview b WHERE a.i = b.i;
+ERROR: VIEW or MATERIALIZED VIEW is not supported on incrementally maintainable materialized view
+CREATE INCREMENTAL MATERIALIZED VIEW mv_ivm09 AS SELECT a.i,a.j FROM mv_base_a a, (SELECT i, COUNT(*) FROM mv_base_b GROUP BY i) b WHERE a.i = b.i;
+ERROR: subquery is not supported on incrementally maintainable materialized view
+CREATE INCREMENTAL MATERIALIZED VIEW mv_ivm10 AS SELECT a.i,a.j FROM mv_base_a a WHERE EXISTS(SELECT 1 FROM mv_base_b b WHERE a.i = b.i) OR a.i > 5;
+ERROR: subquery is not supported on incrementally maintainable materialized view
+-- contain mutable functions
+CREATE INCREMENTAL MATERIALIZED VIEW mv_ivm12 AS SELECT i,j FROM mv_base_a WHERE i = random()::int;
+ERROR: mutable function is not supported on incrementally maintainable materialized view
+HINT: functions must be marked IMMUTABLE
+-- LIMIT/OFFSET is not supported
+CREATE INCREMENTAL MATERIALIZED VIEW mv_ivm13 AS SELECT i,j FROM mv_base_a LIMIT 10 OFFSET 5;
+ERROR: LIMIT/OFFSET clause is not supported on incrementally maintainable materialized view
+-- DISTINCT ON is not supported
+CREATE INCREMENTAL MATERIALIZED VIEW mv_ivm14 AS SELECT DISTINCT ON(i) i, j FROM mv_base_a;
+ERROR: DISTINCT ON is not supported on incrementally maintainable materialized view
+-- TABLESAMPLE clause is not supported
+CREATE INCREMENTAL MATERIALIZED VIEW mv_ivm15 AS SELECT i, j FROM mv_base_a TABLESAMPLE SYSTEM(50);
+ERROR: TABLESAMPLE clause is not supported on incrementally maintainable materialized view
+-- window functions are not supported
+CREATE INCREMENTAL MATERIALIZED VIEW mv_ivm16 AS SELECT *, cume_dist() OVER (ORDER BY i) AS rank FROM mv_base_a;
+ERROR: window functions are not supported on incrementally maintainable materialized view
+-- aggregate function with some options is not supported
+CREATE INCREMENTAL MATERIALIZED VIEW mv_ivm17 AS SELECT COUNT(*) FILTER(WHERE i < 3) FROM mv_base_a;
+ERROR: aggregate function with FILTER clause is not supported on incrementally maintainable materialized view
+CREATE INCREMENTAL MATERIALIZED VIEW mv_ivm18 AS SELECT COUNT(DISTINCT i) FROM mv_base_a;
+ERROR: aggregate function with DISTINCT arguments is not supported on incrementally maintainable materialized view
+CREATE INCREMENTAL MATERIALIZED VIEW mv_ivm19 AS SELECT array_agg(j ORDER BY i DESC) FROM mv_base_a;
+ERROR: aggregate function with ORDER clause is not supported on incrementally maintainable materialized view
+CREATE INCREMENTAL MATERIALIZED VIEW mv_ivm20 AS SELECT i,SUM(j) FROM mv_base_a GROUP BY GROUPING SETS((i),());
+ERROR: GROUPING SETS, ROLLUP, or CUBE clauses is not supported on incrementally maintainable materialized view
+-- inheritance parent is not supported
+BEGIN;
+CREATE TABLE parent (i int, v int);
+NOTICE: Table doesn't have 'DISTRIBUTED BY' clause -- Using column named 'i' as the Cloudberry Database data distribution key for this table.
+HINT: The 'DISTRIBUTED BY' clause determines the distribution of data. Make sure column(s) chosen are the optimal data distribution key to minimize skew.
+CREATE TABLE child_a(options text) INHERITS(parent);
+NOTICE: table has parent, setting distribution columns to match parent table
+CREATE INCREMENTAL MATERIALIZED VIEW mv_ivm21 AS SELECT * FROM parent;
+ERROR: inheritance parent is not supported on incrementally maintainable materialized view
+ROLLBACK;
+-- UNION statement is not supported
+CREATE INCREMENTAL MATERIALIZED VIEW mv_ivm22 AS SELECT i,j FROM mv_base_a UNION ALL SELECT i,k FROM mv_base_b;;
+ERROR: UNION/INTERSECT/EXCEPT statements are not supported on incrementally maintainable materialized view
+-- empty target list is not allowed with IVM
+CREATE INCREMENTAL MATERIALIZED VIEW mv_ivm25 AS SELECT FROM mv_base_a;
+ERROR: empty target list is not supported on incrementally maintainable materialized view
+-- FOR UPDATE/SHARE is not supported
+CREATE INCREMENTAL MATERIALIZED VIEW mv_ivm26 AS SELECT i,j FROM mv_base_a FOR UPDATE;
+ERROR: FOR UPDATE/SHARE clause is not supported on incrementally maintainable materialized view
+-- tartget list cannot contain ivm column that start with '__ivm'
+CREATE INCREMENTAL MATERIALIZED VIEW mv_ivm28 AS SELECT i AS "__ivm_count__" FROM mv_base_a;
+ERROR: column name __ivm_count__ is not supported on incrementally maintainable materialized view
+-- expressions specified in GROUP BY must appear in the target list.
+CREATE INCREMENTAL MATERIALIZED VIEW mv_ivm29 AS SELECT COUNT(i) FROM mv_base_a GROUP BY i;
+ERROR: GROUP BY expression not appearing in select list is not supported on incrementally maintainable materialized view
+-- experssions containing an aggregate is not supported
+CREATE INCREMENTAL MATERIALIZED VIEW mv_ivm30 AS SELECT sum(i)*0.5 FROM mv_base_a;
+ERROR: expression containing an aggregate in it is not supported on incrementally maintainable materialized view
+CREATE INCREMENTAL MATERIALIZED VIEW mv_ivm31 AS SELECT sum(i)/sum(j) FROM mv_base_a;
+ERROR: expression containing an aggregate in it is not supported on incrementally maintainable materialized view
+-- VALUES is not supported
+CREATE INCREMENTAL MATERIALIZED VIEW mv_ivm_only_values1 AS values(1);
+ERROR: VALUES is not supported on incrementally maintainable materialized view
+-- cleanup
+DROP TABLE mv_base_b CASCADE;
+NOTICE: drop cascades to 3 other objects
+DETAIL: drop cascades to materialized view mv_ivm_1
+drop cascades to view b_view
+drop cascades to materialized view b_mview
+DROP TABLE mv_base_a CASCADE;
diff --git a/src/test/regress/expected/ivm.out b/src/test/regress/expected/ivm.out
new file mode 100644
index 00000000000..ffb2a798d70
--- /dev/null
+++ b/src/test/regress/expected/ivm.out
@@ -0,0 +1,133 @@
+DROP TABLE IF EXISTS t0 CASCADE;
+NOTICE: table "t0" does not exist, skipping
+DROP TABLE IF EXISTS t1 CASCADE;
+NOTICE: table "t1" does not exist, skipping
+-- multi insert in udf function
+CREATE TABLE t0 (a int, b int) DISTRIBUTED BY (a);
+CREATE TABLE t1 (a int, b int) DISTRIBUTED BY (a);
+CREATE INCREMENTAL MATERIALIZED VIEW m AS SELECT t0.a, t0.b FROM t0, t1 WHERE t0.a = t1.b DISTRIBUTED BY (a);
+CREATE OR REPLACE FUNCTION insert_t0_and_t1(val integer, v2 integer) RETURNS void AS $$
+BEGIN
+ INSERT INTO t0 VALUES (val, v2);
+ INSERT INTO t1 VALUES (val, v2);
+END;
+$$ LANGUAGE plpgsql;
+select insert_t0_and_t1(1,1);
+ insert_t0_and_t1
+------------------
+
+(1 row)
+
+select insert_t0_and_t1(2,2);
+ insert_t0_and_t1
+------------------
+
+(1 row)
+
+select insert_t0_and_t1(3,3);
+ insert_t0_and_t1
+------------------
+
+(1 row)
+
+select * from m order by 1;
+ a | b
+---+---
+ 1 | 1
+ 2 | 2
+ 3 | 3
+(3 rows)
+
+-- use rule to trigger insert
+DROP TABLE t0 CASCADE;
+NOTICE: drop cascades to materialized view m
+DROP TABLE t1 CASCADE;
+CREATE TABLE t0 (a int, b int) DISTRIBUTED BY (a);
+CREATE TABLE t1 (a int, b int) DISTRIBUTED BY (a);
+CREATE INCREMENTAL MATERIALIZED VIEW m AS SELECT t0.a FROM t0, t1 WHERE t0.a = t1.a DISTRIBUTED BY (a);
+CREATE RULE insert_t0_and_t1 AS
+ ON INSERT TO t0 DO ALSO
+ INSERT INTO t1 (a, b) VALUES (NEW.a, NEW.b);
+INSERT INTO t0 VALUES (1,1);
+INSERT INTO t0 VALUES (2,2);
+INSERT INTO t0 VALUES (3,3);
+select * from m order by 1;
+ a
+---
+ 1
+ 2
+ 3
+(3 rows)
+
+-- one base table and multi incremental view
+DROP TABLE t0 CASCADE;
+NOTICE: drop cascades to materialized view m
+DROP TABLE t1 CASCADE;
+CREATE TABLE t0 (a int) DISTRIBUTED BY (a);
+CREATE INCREMENTAL MATERIALIZED VIEW m1 AS SELECT sum(a) FROM t0;
+NOTICE: Table doesn't have 'DISTRIBUTED BY' clause -- Using column(s) named 'sum' as the Cloudberry Database data distribution key for this table.
+HINT: The 'DISTRIBUTED BY' clause determines the distribution of data. Make sure column(s) chosen are the optimal data distribution key to minimize skew.
+CREATE INCREMENTAL MATERIALIZED VIEW m2 AS SELECT sum(a) FROM t0;
+NOTICE: Table doesn't have 'DISTRIBUTED BY' clause -- Using column(s) named 'sum' as the Cloudberry Database data distribution key for this table.
+HINT: The 'DISTRIBUTED BY' clause determines the distribution of data. Make sure column(s) chosen are the optimal data distribution key to minimize skew.
+BEGIN;
+INSERT INTO t0 VALUES (1), (2);
+INSERT INTO t0 VALUES (3);
+COMMIT;
+SELECT * FROM m1 order by 1;
+ sum
+-----
+ 6
+(1 row)
+
+SELECT * FROM m2 order by 1;
+ sum
+-----
+ 6
+(1 row)
+
+-- abort transaction
+BEGIN;
+INSERT INTO t0 VALUES (4);
+ABORT;
+SELECT * FROM m1;
+ sum
+-----
+ 6
+(1 row)
+
+-- subtransaction
+BEGIN;
+INSERT INTO t0 VALUES (5);
+SAVEPOINT p1;
+INSERT INTO t0 VALUES (6);
+ROLLBACK TO SAVEPOINT p1;
+INSERT INTO t0 VALUES (6);
+SELECT * FROM m1;
+ sum
+-----
+ 17
+(1 row)
+
+COMMIT;
+-- test copy
+COPY t0 (a) from stdin;
+-- test insert batch
+INSERT INTO t0 SELECT * FROM generate_series(11, 100000);
+SELECT * FROM m1;
+ sum
+------------
+ 5000049996
+(1 row)
+
+SELECT * FROM m2;
+ sum
+------------
+ 5000049996
+(1 row)
+
+-- cleanup
+DROP TABLE t0 CASCADE;
+NOTICE: drop cascades to 2 other objects
+DETAIL: drop cascades to materialized view m1
+drop cascades to materialized view m2
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 7760160f463..792c667348f 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -109,7 +109,12 @@ test: task
# ----------
# Another group of parallel tests
# ----------
-test: create_table_like alter_generic alter_operator misc async dbsize misc_functions sysviews tsrf tid tidscan tidrangescan collate.icu.utf8 incremental_sort
+test: create_table_like alter_generic alter_operator misc async dbsize misc_functions sysviews tsrf tid tidscan tidrangescan collate.icu.utf8 incremental_sort incremental_matview
+
+# ----------
+# Additional incremental view maintenance tests
+# ----------
+test: ivm
# rules cannot run concurrently with any test that creates
# a view or rule in the public schema
diff --git a/src/test/regress/sql/incremental_matview.sql b/src/test/regress/sql/incremental_matview.sql
new file mode 100644
index 00000000000..a7342e13fbd
--- /dev/null
+++ b/src/test/regress/sql/incremental_matview.sql
@@ -0,0 +1,457 @@
+-- create a table to use as a basis for views and materialized views in various combinations
+CREATE TABLE mv_base_a (i int, j int) DISTRIBUTED BY (i);
+INSERT INTO mv_base_a VALUES
+ (1,10),
+ (2,20),
+ (3,30),
+ (4,40),
+ (5,50);
+CREATE TABLE mv_base_b (i int, k int) DISTRIBUTED BY (i);
+INSERT INTO mv_base_b VALUES
+ (1,101),
+ (2,102),
+ (3,103),
+ (4,104);
+
+CREATE INCREMENTAL MATERIALIZED VIEW mv_ivm_1 AS SELECT i,j,k FROM mv_base_a a INNER JOIN mv_base_b b USING(i) WITH NO DATA;
+SELECT * FROM mv_ivm_1 ORDER BY 1,2,3;
+REFRESH MATERIALIZED VIEW mv_ivm_1;
+SELECT * FROM mv_ivm_1 ORDER BY 1,2,3;
+
+-- REFRESH WITH NO DATA
+BEGIN;
+CREATE FUNCTION dummy_ivm_trigger_func() RETURNS TRIGGER AS $$
+ BEGIN
+ RETURN NULL;
+ END
+$$ language plpgsql;
+
+CREATE CONSTRAINT TRIGGER dummy_ivm_trigger AFTER INSERT
+ON mv_base_a FROM mv_ivm_1 FOR EACH ROW
+EXECUTE PROCEDURE dummy_ivm_trigger_func();
+
+SELECT COUNT(*)
+FROM pg_depend pd INNER JOIN pg_trigger pt ON pd.objid = pt.oid
+WHERE pd.classid = 'pg_trigger'::regclass AND pd.refobjid = 'mv_ivm_1'::regclass;
+
+REFRESH MATERIALIZED VIEW mv_ivm_1 WITH NO DATA;
+
+SELECT COUNT(*)
+FROM pg_depend pd INNER JOIN pg_trigger pt ON pd.objid = pt.oid
+WHERE pd.classid = 'pg_trigger'::regclass AND pd.refobjid = 'mv_ivm_1'::regclass;
+ROLLBACK;
+
+-- immediate maintenance
+BEGIN;
+INSERT INTO mv_base_b VALUES(5,105);
+SELECT * FROM mv_ivm_1 ORDER BY 1,2,3;
+UPDATE mv_base_a SET j = 0 WHERE i = 1;
+SELECT * FROM mv_ivm_1 ORDER BY 1,2,3;
+DELETE FROM mv_base_b WHERE (i,k) = (5,105);
+SELECT * FROM mv_ivm_1 ORDER BY 1,2,3;
+ROLLBACK;
+SELECT * FROM mv_ivm_1 ORDER BY 1,2,3;
+
+-- rename of IVM columns
+CREATE INCREMENTAL MATERIALIZED VIEW mv_ivm_rename AS SELECT DISTINCT * FROM mv_base_a;
+ALTER MATERIALIZED VIEW mv_ivm_rename RENAME COLUMN __ivm_count__ TO xxx;
+DROP MATERIALIZED VIEW mv_ivm_rename;
+
+-- unique index on IVM columns
+CREATE INCREMENTAL MATERIALIZED VIEW mv_ivm_unique AS SELECT DISTINCT * FROM mv_base_a;
+CREATE UNIQUE INDEX ON mv_ivm_unique(__ivm_count__);
+CREATE UNIQUE INDEX ON mv_ivm_unique((__ivm_count__));
+CREATE UNIQUE INDEX ON mv_ivm_unique((__ivm_count__ + 1));
+DROP MATERIALIZED VIEW mv_ivm_unique;
+
+-- TRUNCATE a base table in join views
+BEGIN;
+TRUNCATE mv_base_a;
+SELECT * FROM mv_ivm_1;
+ROLLBACK;
+
+BEGIN;
+TRUNCATE mv_base_b;
+SELECT * FROM mv_ivm_1;
+ROLLBACK;
+
+-- some query syntax
+BEGIN;
+CREATE FUNCTION ivm_func() RETURNS int LANGUAGE 'sql'
+ AS 'SELECT 1' IMMUTABLE;
+CREATE INCREMENTAL MATERIALIZED VIEW mv_ivm_func AS SELECT * FROM ivm_func();
+CREATE INCREMENTAL MATERIALIZED VIEW mv_ivm_no_tbl AS SELECT 1;
+ROLLBACK;
+
+-- result of materialized view have DISTINCT clause or the duplicate result.
+BEGIN;
+CREATE INCREMENTAL MATERIALIZED VIEW mv_ivm_duplicate AS SELECT j FROM mv_base_a;
+CREATE INCREMENTAL MATERIALIZED VIEW mv_ivm_distinct AS SELECT DISTINCT j FROM mv_base_a;
+INSERT INTO mv_base_a VALUES(6,20);
+SELECT * FROM mv_ivm_duplicate ORDER BY 1;
+SELECT * FROM mv_ivm_distinct ORDER BY 1;
+DELETE FROM mv_base_a WHERE (i,j) = (2,20);
+SELECT * FROM mv_ivm_duplicate ORDER BY 1;
+SELECT * FROM mv_ivm_distinct ORDER BY 1;
+ROLLBACK;
+
+-- support SUM(), COUNT() and AVG() aggregate functions
+BEGIN;
+CREATE INCREMENTAL MATERIALIZED VIEW mv_ivm_agg AS SELECT i, SUM(j), COUNT(i), AVG(j) FROM mv_base_a GROUP BY i;
+SELECT * FROM mv_ivm_agg ORDER BY 1,2,3,4;
+INSERT INTO mv_base_a VALUES(2,100);
+SELECT * FROM mv_ivm_agg ORDER BY 1,2,3,4;
+UPDATE mv_base_a SET j = 200 WHERE (i,j) = (2,100);
+SELECT * FROM mv_ivm_agg ORDER BY 1,2,3,4;
+DELETE FROM mv_base_a WHERE (i,j) = (2,200);
+SELECT * FROM mv_ivm_agg ORDER BY 1,2,3,4;
+ROLLBACK;
+
+-- support COUNT(*) aggregate function
+BEGIN;
+CREATE INCREMENTAL MATERIALIZED VIEW mv_ivm_agg AS SELECT i, SUM(j), COUNT(*) FROM mv_base_a GROUP BY i;
+SELECT * FROM mv_ivm_agg ORDER BY 1,2,3;
+INSERT INTO mv_base_a VALUES(2,100);
+SELECT * FROM mv_ivm_agg ORDER BY 1,2,3;
+ROLLBACK;
+
+-- TRUNCATE a base table in aggregate views
+BEGIN;
+CREATE INCREMENTAL MATERIALIZED VIEW mv_ivm_agg AS SELECT i, SUM(j), COUNT(*) FROM mv_base_a GROUP BY i;
+TRUNCATE mv_base_a;
+SELECT sum, count FROM mv_ivm_agg;
+SELECT i, SUM(j), COUNT(*) FROM mv_base_a GROUP BY i;
+ROLLBACK;
+
+-- support aggregate functions without GROUP clause
+BEGIN;
+CREATE INCREMENTAL MATERIALIZED VIEW mv_ivm_group AS SELECT SUM(j), COUNT(j), AVG(j) FROM mv_base_a;
+SELECT * FROM mv_ivm_group ORDER BY 1;
+INSERT INTO mv_base_a VALUES(6,60);
+SELECT * FROM mv_ivm_group ORDER BY 1;
+DELETE FROM mv_base_a;
+SELECT * FROM mv_ivm_group ORDER BY 1;
+ROLLBACK;
+
+-- TRUNCATE a base table in aggregate views without GROUP clause
+BEGIN;
+CREATE INCREMENTAL MATERIALIZED VIEW mv_ivm_group AS SELECT SUM(j), COUNT(j), AVG(j) FROM mv_base_a;
+TRUNCATE mv_base_a;
+SELECT sum, count, avg FROM mv_ivm_group;
+SELECT SUM(j), COUNT(j), AVG(j) FROM mv_base_a;
+ROLLBACK;
+
+-- resolved issue: When use AVG() function and values is indivisible, result of AVG() is incorrect.
+BEGIN;
+CREATE INCREMENTAL MATERIALIZED VIEW mv_ivm_avg_bug AS SELECT i, SUM(j), COUNT(j), AVG(j) FROM mv_base_A GROUP BY i;
+SELECT * FROM mv_ivm_avg_bug ORDER BY 1,2,3;
+INSERT INTO mv_base_a VALUES
+ (1,0),
+ (1,0),
+ (2,30),
+ (2,30);
+SELECT * FROM mv_ivm_avg_bug ORDER BY 1,2,3;
+DELETE FROM mv_base_a WHERE (i,j) = (1,0);
+DELETE FROM mv_base_a WHERE (i,j) = (2,30);
+SELECT * FROM mv_ivm_avg_bug ORDER BY 1,2,3;
+ROLLBACK;
+
+-- aggregate views with column names specified
+BEGIN;
+CREATE INCREMENTAL MATERIALIZED VIEW mv_ivm_agg(a) AS SELECT i, SUM(j) FROM mv_base_a GROUP BY i;
+INSERT INTO mv_base_a VALUES (1,100), (2,200), (3,300);
+UPDATE mv_base_a SET j = 2000 WHERE (i,j) = (2,20);
+DELETE FROM mv_base_a WHERE (i,j) = (3,30);
+SELECT * FROM mv_ivm_agg ORDER BY 1,2;
+ROLLBACK;
+BEGIN;
+CREATE INCREMENTAL MATERIALIZED VIEW mv_ivm_agg(a,b) AS SELECT i, SUM(j) FROM mv_base_a GROUP BY i;
+INSERT INTO mv_base_a VALUES (1,100), (2,200), (3,300);
+UPDATE mv_base_a SET j = 2000 WHERE (i,j) = (2,20);
+DELETE FROM mv_base_a WHERE (i,j) = (3,30);
+SELECT * FROM mv_ivm_agg ORDER BY 1,2;
+ROLLBACK;
+BEGIN;
+CREATE INCREMENTAL MATERIALIZED VIEW mv_ivm_agg(a,b,c) AS SELECT i, SUM(j) FROM mv_base_a GROUP BY i;
+ROLLBACK;
+
+-- support self join view and multiple change on the same table
+BEGIN;
+CREATE TABLE base_t (i int, v int) DISTRIBUTED BY (i);
+INSERT INTO base_t VALUES (1, 10), (2, 20), (3, 30);
+CREATE INCREMENTAL MATERIALIZED VIEW mv_self(v1, v2) AS
+ SELECT t1.v, t2.v FROM base_t AS t1 JOIN base_t AS t2 ON t1.i = t2.i;
+SELECT * FROM mv_self ORDER BY v1;
+INSERT INTO base_t VALUES (4,40);
+DELETE FROM base_t WHERE i = 1;
+UPDATE base_t SET v = v*10 WHERE i=2;
+SELECT * FROM mv_self ORDER BY v1;
+
+--- with sub-transactions
+SAVEPOINT p1;
+INSERT INTO base_t VALUES (7,70);
+RELEASE SAVEPOINT p1;
+INSERT INTO base_t VALUES (7,77);
+SELECT * FROM mv_self ORDER BY v1, v2;
+
+ROLLBACK;
+
+
+-- views including NULL
+BEGIN;
+CREATE TABLE base_t (i int, v int) DISTRIBUTED BY (i);
+INSERT INTO base_t VALUES (1,10),(2, NULL);
+CREATE INCREMENTAL MATERIALIZED VIEW mv AS SELECT * FROM base_t;
+SELECT * FROM mv ORDER BY i;
+UPDATE base_t SET v = 20 WHERE i = 2;
+SELECT * FROM mv ORDER BY i;
+ROLLBACK;
+
+BEGIN;
+CREATE TABLE base_t (i int) DISTRIBUTED BY (i);
+CREATE INCREMENTAL MATERIALIZED VIEW mv AS SELECT * FROM base_t;
+SELECT * FROM mv ORDER BY i;
+INSERT INTO base_t VALUES (1),(NULL);
+SELECT * FROM mv ORDER BY i;
+ROLLBACK;
+
+BEGIN;
+CREATE TABLE base_t (i int, v int) DISTRIBUTED BY (i);
+INSERT INTO base_t VALUES (NULL, 1), (NULL, 2), (1, 10), (1, 20);
+CREATE INCREMENTAL MATERIALIZED VIEW mv AS SELECT i, sum(v) FROM base_t GROUP BY i;
+SELECT * FROM mv ORDER BY i;
+UPDATE base_t SET v = v * 10;
+SELECT * FROM mv ORDER BY i;
+ROLLBACK;
+
+-- support joins
+BEGIN;
+CREATE TABLE base_r(i int) DISTRIBUTED BY (i);
+CREATE TABLE base_s (i int, j int) DISTRIBUTED BY (i);
+CREATE TABLE base_t (j int) DISTRIBUTED BY (j);
+INSERT INTO base_r VALUES (1), (2), (3), (3);
+INSERT INTO base_s VALUES (2,1), (2,2), (3,1), (4,1), (4,2);
+INSERT INTO base_t VALUES (2), (3), (3);
+
+CREATE FUNCTION is_match() RETURNS text AS $$
+DECLARE
+x text;
+BEGIN
+ EXECUTE
+ 'SELECT CASE WHEN count(*) = 0 THEN ''OK'' ELSE ''NG'' END FROM (
+ SELECT * FROM (SELECT * FROM mv EXCEPT ALL SELECT * FROM v) v1
+ UNION ALL
+ SELECT * FROM (SELECT * FROM v EXCEPT ALL SELECT * FROM mv) v2
+ ) v' INTO x;
+ RETURN x;
+END;
+$$ LANGUAGE plpgsql;
+
+-- 3-way join
+CREATE INCREMENTAL MATERIALIZED VIEW mv(r, si, sj, t) AS
+ SELECT r.i, s.i, s.j, t.j
+ FROM base_r AS r JOIN base_s AS s ON r.i=s.i JOIN base_t AS t ON s.j=t.j;
+CREATE VIEW v(r, si, sj, t) AS
+ SELECT r.i, s.i, s.j, t.j
+ FROM base_r AS r JOIN base_s AS s ON r.i=s.i JOIN base_t AS t ON s.j=t.j;
+SELECT * FROM mv ORDER BY r, si, sj, t;
+SAVEPOINT p1;
+
+INSERT INTO base_r VALUES (1),(2),(3);
+SELECT * FROM mv ORDER BY r, si, sj, t;
+SELECT is_match();
+INSERT INTO base_r VALUES (4),(5);
+SELECT * FROM mv ORDER BY r, si, sj, t;
+SELECT is_match();
+ROLLBACK TO p1;
+
+INSERT INTO base_s VALUES (1,3);
+SELECT * FROM mv ORDER BY r, si, sj, t;
+SELECT is_match();
+INSERT INTO base_s VALUES (2,3);
+SELECT * FROM mv ORDER BY r, si, sj, t;
+SELECT is_match();
+ROLLBACK TO p1;
+
+INSERT INTO base_t VALUES (1),(2);
+SELECT * FROM mv ORDER BY r, si, sj, t;
+SELECT is_match();
+INSERT INTO base_t VALUES (3),(4);
+SELECT * FROM mv ORDER BY r, si, sj, t;
+SELECT is_match();
+ROLLBACK TO p1;
+
+DELETE FROM base_r WHERE i=1;
+SELECT * FROM mv ORDER BY r, si, sj, t;
+SELECT is_match();
+DELETE FROM base_r WHERE i=2;
+SELECT * FROM mv ORDER BY r, si, sj, t;
+SELECT is_match();
+DELETE FROM base_r WHERE i=3;
+SELECT * FROM mv ORDER BY r, si, sj, t;
+SELECT is_match();
+ROLLBACK TO p1;
+
+DELETE FROM base_s WHERE i=2;
+SELECT * FROM mv ORDER BY r, si, sj, t;
+SELECT is_match();
+DELETE FROM base_s WHERE i=3;
+SELECT * FROM mv ORDER BY r, si, sj, t;
+SELECT is_match();
+DELETE FROM base_s WHERE i=4;
+SELECT * FROM mv ORDER BY r, si, sj, t;
+SELECT is_match();
+ROLLBACK TO p1;
+
+DELETE FROM base_t WHERE j=2;
+SELECT * FROM mv ORDER BY r, si, sj, t;
+SELECT is_match();
+DELETE FROM base_t WHERE j=3;
+SELECT * FROM mv ORDER BY r, si, sj, t;
+SELECT is_match();
+ROLLBACK TO p1;
+
+-- TRUNCATE a base table in views
+TRUNCATE base_r;
+SELECT is_match();
+ROLLBACK TO p1;
+
+TRUNCATE base_s;
+SELECT is_match();
+ROLLBACK TO p1;
+
+TRUNCATE base_t;
+SELECT is_match();
+ROLLBACK TO p1;
+
+DROP MATERIALIZED VIEW mv;
+DROP VIEW v;
+ROLLBACK;
+
+-- IMMV containing user defined type
+BEGIN;
+
+CREATE TYPE mytype;
+CREATE FUNCTION mytype_in(cstring)
+ RETURNS mytype AS 'int4in'
+ LANGUAGE INTERNAL STRICT IMMUTABLE;
+CREATE FUNCTION mytype_out(mytype)
+ RETURNS cstring AS 'int4out'
+ LANGUAGE INTERNAL STRICT IMMUTABLE;
+CREATE TYPE mytype (
+ LIKE = int4,
+ INPUT = mytype_in,
+ OUTPUT = mytype_out
+);
+
+CREATE FUNCTION mytype_eq(mytype, mytype)
+ RETURNS bool AS 'int4eq'
+ LANGUAGE INTERNAL STRICT IMMUTABLE;
+CREATE FUNCTION mytype_lt(mytype, mytype)
+ RETURNS bool AS 'int4lt'
+ LANGUAGE INTERNAL STRICT IMMUTABLE;
+CREATE FUNCTION mytype_cmp(mytype, mytype)
+ RETURNS integer AS 'btint4cmp'
+ LANGUAGE INTERNAL STRICT IMMUTABLE;
+
+CREATE OPERATOR = (
+ leftarg = mytype, rightarg = mytype,
+ procedure = mytype_eq);
+CREATE OPERATOR < (
+ leftarg = mytype, rightarg = mytype,
+ procedure = mytype_lt);
+
+CREATE OPERATOR CLASS mytype_ops
+ DEFAULT FOR TYPE mytype USING btree AS
+ OPERATOR 1 <,
+ OPERATOR 3 = ,
+ FUNCTION 1 mytype_cmp(mytype,mytype);
+
+CREATE TABLE t_mytype (x mytype);
+CREATE INCREMENTAL MATERIALIZED VIEW mv_mytype AS
+ SELECT * FROM t_mytype;
+INSERT INTO t_mytype VALUES ('1'::mytype);
+SELECT * FROM mv_mytype;
+
+ROLLBACK;
+
+-- outer join is not supported
+CREATE INCREMENTAL MATERIALIZED VIEW mv(a,b) AS SELECT a.i, b.i FROM mv_base_a a LEFT JOIN mv_base_b b ON a.i=b.i;
+-- CTE is not supported
+CREATE INCREMENTAL MATERIALIZED VIEW mv AS
+ WITH b AS ( SELECT * FROM mv_base_b) SELECT a.i,a.j FROM mv_base_a a, b WHERE a.i = b.i;
+-- contain system column
+CREATE INCREMENTAL MATERIALIZED VIEW mv_ivm01 AS SELECT i,j,xmin FROM mv_base_a;
+CREATE INCREMENTAL MATERIALIZED VIEW mv_ivm02 AS SELECT i,j FROM mv_base_a WHERE xmin = '610';
+CREATE INCREMENTAL MATERIALIZED VIEW mv_ivm04 AS SELECT i,j,xmin::text AS x_min FROM mv_base_a;
+CREATE INCREMENTAL MATERIALIZED VIEW mv_ivm06 AS SELECT i,j,xidsend(xmin) AS x_min FROM mv_base_a;
+-- contain subquery
+CREATE INCREMENTAL MATERIALIZED VIEW mv_ivm03 AS SELECT i,j FROM mv_base_a WHERE i IN (SELECT i FROM mv_base_b WHERE k < 103 );
+CREATE INCREMENTAL MATERIALIZED VIEW mv_ivm04 AS SELECT a.i,a.j FROM mv_base_a a, (SELECT * FROM mv_base_b) b WHERE a.i = b.i;
+CREATE INCREMENTAL MATERIALIZED VIEW mv_ivm05 AS SELECT i,j, (SELECT k FROM mv_base_b b WHERE a.i = b.i) FROM mv_base_a a;
+-- contain ORDER BY
+CREATE INCREMENTAL MATERIALIZED VIEW mv_ivm07 AS SELECT i,j,k FROM mv_base_a a INNER JOIN mv_base_b b USING(i) ORDER BY i,j,k;
+-- contain HAVING
+CREATE INCREMENTAL MATERIALIZED VIEW mv_ivm08 AS SELECT i,j,k FROM mv_base_a a INNER JOIN mv_base_b b USING(i) GROUP BY i,j,k HAVING SUM(i) > 5;
+
+-- contain view or materialized view
+CREATE VIEW b_view AS SELECT i,k FROM mv_base_b;
+CREATE MATERIALIZED VIEW b_mview AS SELECT i,k FROM mv_base_b;
+CREATE INCREMENTAL MATERIALIZED VIEW mv_ivm07 AS SELECT a.i,a.j FROM mv_base_a a,b_view b WHERE a.i = b.i;
+CREATE INCREMENTAL MATERIALIZED VIEW mv_ivm08 AS SELECT a.i,a.j FROM mv_base_a a,b_mview b WHERE a.i = b.i;
+CREATE INCREMENTAL MATERIALIZED VIEW mv_ivm09 AS SELECT a.i,a.j FROM mv_base_a a, (SELECT i, COUNT(*) FROM mv_base_b GROUP BY i) b WHERE a.i = b.i;
+CREATE INCREMENTAL MATERIALIZED VIEW mv_ivm10 AS SELECT a.i,a.j FROM mv_base_a a WHERE EXISTS(SELECT 1 FROM mv_base_b b WHERE a.i = b.i) OR a.i > 5;
+
+-- contain mutable functions
+CREATE INCREMENTAL MATERIALIZED VIEW mv_ivm12 AS SELECT i,j FROM mv_base_a WHERE i = random()::int;
+
+-- LIMIT/OFFSET is not supported
+CREATE INCREMENTAL MATERIALIZED VIEW mv_ivm13 AS SELECT i,j FROM mv_base_a LIMIT 10 OFFSET 5;
+
+-- DISTINCT ON is not supported
+CREATE INCREMENTAL MATERIALIZED VIEW mv_ivm14 AS SELECT DISTINCT ON(i) i, j FROM mv_base_a;
+
+-- TABLESAMPLE clause is not supported
+CREATE INCREMENTAL MATERIALIZED VIEW mv_ivm15 AS SELECT i, j FROM mv_base_a TABLESAMPLE SYSTEM(50);
+
+-- window functions are not supported
+CREATE INCREMENTAL MATERIALIZED VIEW mv_ivm16 AS SELECT *, cume_dist() OVER (ORDER BY i) AS rank FROM mv_base_a;
+
+-- aggregate function with some options is not supported
+CREATE INCREMENTAL MATERIALIZED VIEW mv_ivm17 AS SELECT COUNT(*) FILTER(WHERE i < 3) FROM mv_base_a;
+CREATE INCREMENTAL MATERIALIZED VIEW mv_ivm18 AS SELECT COUNT(DISTINCT i) FROM mv_base_a;
+CREATE INCREMENTAL MATERIALIZED VIEW mv_ivm19 AS SELECT array_agg(j ORDER BY i DESC) FROM mv_base_a;
+CREATE INCREMENTAL MATERIALIZED VIEW mv_ivm20 AS SELECT i,SUM(j) FROM mv_base_a GROUP BY GROUPING SETS((i),());
+
+-- inheritance parent is not supported
+BEGIN;
+CREATE TABLE parent (i int, v int);
+CREATE TABLE child_a(options text) INHERITS(parent);
+CREATE INCREMENTAL MATERIALIZED VIEW mv_ivm21 AS SELECT * FROM parent;
+ROLLBACK;
+
+-- UNION statement is not supported
+CREATE INCREMENTAL MATERIALIZED VIEW mv_ivm22 AS SELECT i,j FROM mv_base_a UNION ALL SELECT i,k FROM mv_base_b;;
+
+-- empty target list is not allowed with IVM
+CREATE INCREMENTAL MATERIALIZED VIEW mv_ivm25 AS SELECT FROM mv_base_a;
+
+-- FOR UPDATE/SHARE is not supported
+CREATE INCREMENTAL MATERIALIZED VIEW mv_ivm26 AS SELECT i,j FROM mv_base_a FOR UPDATE;
+
+-- tartget list cannot contain ivm column that start with '__ivm'
+CREATE INCREMENTAL MATERIALIZED VIEW mv_ivm28 AS SELECT i AS "__ivm_count__" FROM mv_base_a;
+
+-- expressions specified in GROUP BY must appear in the target list.
+CREATE INCREMENTAL MATERIALIZED VIEW mv_ivm29 AS SELECT COUNT(i) FROM mv_base_a GROUP BY i;
+
+-- experssions containing an aggregate is not supported
+CREATE INCREMENTAL MATERIALIZED VIEW mv_ivm30 AS SELECT sum(i)*0.5 FROM mv_base_a;
+CREATE INCREMENTAL MATERIALIZED VIEW mv_ivm31 AS SELECT sum(i)/sum(j) FROM mv_base_a;
+
+-- VALUES is not supported
+CREATE INCREMENTAL MATERIALIZED VIEW mv_ivm_only_values1 AS values(1);
+
+-- cleanup
+DROP TABLE mv_base_b CASCADE;
+DROP TABLE mv_base_a CASCADE;
diff --git a/src/test/regress/sql/ivm.sql b/src/test/regress/sql/ivm.sql
new file mode 100644
index 00000000000..fc864f73039
--- /dev/null
+++ b/src/test/regress/sql/ivm.sql
@@ -0,0 +1,75 @@
+
+DROP TABLE IF EXISTS t0 CASCADE;
+DROP TABLE IF EXISTS t1 CASCADE;
+
+-- multi insert in udf function
+CREATE TABLE t0 (a int, b int) DISTRIBUTED BY (a);
+CREATE TABLE t1 (a int, b int) DISTRIBUTED BY (a);
+CREATE INCREMENTAL MATERIALIZED VIEW m AS SELECT t0.a, t0.b FROM t0, t1 WHERE t0.a = t1.b DISTRIBUTED BY (a);
+CREATE OR REPLACE FUNCTION insert_t0_and_t1(val integer, v2 integer) RETURNS void AS $$
+BEGIN
+ INSERT INTO t0 VALUES (val, v2);
+ INSERT INTO t1 VALUES (val, v2);
+END;
+$$ LANGUAGE plpgsql;
+select insert_t0_and_t1(1,1);
+select insert_t0_and_t1(2,2);
+select insert_t0_and_t1(3,3);
+select * from m order by 1;
+
+-- use rule to trigger insert
+DROP TABLE t0 CASCADE;
+DROP TABLE t1 CASCADE;
+CREATE TABLE t0 (a int, b int) DISTRIBUTED BY (a);
+CREATE TABLE t1 (a int, b int) DISTRIBUTED BY (a);
+CREATE INCREMENTAL MATERIALIZED VIEW m AS SELECT t0.a FROM t0, t1 WHERE t0.a = t1.a DISTRIBUTED BY (a);
+CREATE RULE insert_t0_and_t1 AS
+ ON INSERT TO t0 DO ALSO
+ INSERT INTO t1 (a, b) VALUES (NEW.a, NEW.b);
+INSERT INTO t0 VALUES (1,1);
+INSERT INTO t0 VALUES (2,2);
+INSERT INTO t0 VALUES (3,3);
+select * from m order by 1;
+
+
+-- one base table and multi incremental view
+DROP TABLE t0 CASCADE;
+DROP TABLE t1 CASCADE;
+
+CREATE TABLE t0 (a int) DISTRIBUTED BY (a);
+CREATE INCREMENTAL MATERIALIZED VIEW m1 AS SELECT sum(a) FROM t0;
+CREATE INCREMENTAL MATERIALIZED VIEW m2 AS SELECT sum(a) FROM t0;
+BEGIN;
+INSERT INTO t0 VALUES (1), (2);
+INSERT INTO t0 VALUES (3);
+COMMIT;
+SELECT * FROM m1 order by 1;
+SELECT * FROM m2 order by 1;
+-- abort transaction
+BEGIN;
+INSERT INTO t0 VALUES (4);
+ABORT;
+SELECT * FROM m1;
+-- subtransaction
+BEGIN;
+INSERT INTO t0 VALUES (5);
+SAVEPOINT p1;
+INSERT INTO t0 VALUES (6);
+ROLLBACK TO SAVEPOINT p1;
+INSERT INTO t0 VALUES (6);
+SELECT * FROM m1;
+COMMIT;
+-- test copy
+COPY t0 (a) from stdin;
+7
+8
+9
+10
+\.
+-- test insert batch
+INSERT INTO t0 SELECT * FROM generate_series(11, 100000);
+SELECT * FROM m1;
+SELECT * FROM m2;
+
+-- cleanup
+DROP TABLE t0 CASCADE;