diff --git a/README.md b/README.md index 1bd1d3d..2e6371f 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ The `pg_ivm` module provides Incremental View Maintenance (IVM) feature for PostgreSQL. -The extension is compatible with PostgreSQL 13, 14, 15, and 16. +The extension is compatible with PostgreSQL 13, 14, 15, 16, and 17. ## Description @@ -54,6 +54,8 @@ postgres=# SELECT * FROM m; -- automatically updated (4 rows) ``` +Note that if you use PostgreSQL 17 or later, during automatic maintenance of an IMMV, the `search_path` is temporarily changed to `pg_catalog, pg_temp`. + ## Installation To install `pg_ivm`, execute this in the module's directory: @@ -97,10 +99,12 @@ Use `refresh_immv` function to refresh IMMV. refresh_immv(immv_name text, with_data bool) RETURNS bigint ``` -`refresh_immv` completely replaces the contents of an IMMV as `REFRESH MATERIALIZED VIEW` command does for a materialized view. To execute this function you must be the owner of the IMMV. The old contents are discarded. +`refresh_immv` completely replaces the contents of an IMMV as `REFRESH MATERIALIZED VIEW` command does for a materialized view. To execute this function you must be the owner of the IMMV (with PostgreSQL 16 or earlier) or have the `MAINTAIN` privilege on the IMMV (with PostgreSQL 17 or later). The old contents are discarded. The with_data flag is corresponding to `WITH [NO] DATA` option of REFRESH MATERIALIZED VIEW` command. If with_data is true, the backing query is executed to provide the new data, and if the IMMV is 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_data is false, no new data is generated and the IMMV become unpopulated, and the triggers are dropped from the IMMV. Note that unpopulated IMMV is still scannable although the result is empty. This behaviour may be changed in future to raise an error when an unpopulated IMMV is scanned. +Note that if you use PostgreSQL 17 or later, while `refresh_immv` is running, the `search_path` is temporarily changed to `pg_catalog, pg_temp`. + #### get_immv_def `get_immv_def` reconstructs the underlying SELECT command for an IMMV. (This is a decompiled reconstruction, not the original text of the command.) diff --git a/createas.c b/createas.c index dba81c8..6218d16 100644 --- a/createas.c +++ b/createas.c @@ -11,9 +11,9 @@ */ #include "postgres.h" -#include "access/xact.h" -#include "access/genam.h" #include "access/heapam.h" +#include "access/reloptions.h" +#include "access/xact.h" #include "catalog/dependency.h" #include "catalog/index.h" #include "catalog/indexing.h" @@ -21,16 +21,15 @@ #include "catalog/pg_constraint.h" #include "catalog/pg_inherits.h" #include "catalog/pg_trigger_d.h" +#include "catalog/toasting.h" #include "commands/createas.h" #include "commands/defrem.h" +#include "commands/tablecmds.h" #include "commands/tablespace.h" #include "commands/trigger.h" -#include "executor/execdesc.h" -#include "executor/executor.h" #include "miscadmin.h" #include "nodes/makefuncs.h" #include "nodes/nodeFuncs.h" -#include "nodes/pathnodes.h" #include "optimizer/optimizer.h" #include "optimizer/prep.h" #include "parser/parser.h" @@ -40,12 +39,11 @@ #include "parser/parse_type.h" #include "rewrite/rewriteHandler.h" #include "rewrite/rewriteManip.h" -#include "tcop/tcopprot.h" #include "utils/builtins.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/regproc.h" -#include "utils/snapmgr.h" +#include "utils/rel.h" #include "utils/syscache.h" #include "pg_ivm.h" @@ -62,6 +60,9 @@ typedef struct BulkInsertState bistate; /* bulk insert state */ } DR_intorel; +/* utility functions for IMMV definition creation */ +static ObjectAddress create_immv_internal(List *attrList, IntoClause *into); +static ObjectAddress create_immv_nodata(List *tlist, IntoClause *into); typedef struct { @@ -80,201 +81,225 @@ static bool check_ivm_restriction_walker(Node *node, check_ivm_restriction_conte static Bitmapset *get_primary_key_attnos_from_query(Query *query, List **constraintList); static bool check_aggregate_supports_ivm(Oid aggfnoid); -static void StoreImmvQuery(Oid viewOid, bool ispopulated, Query *viewQuery); +static void StoreImmvQuery(Oid viewOid, Query *viewQuery); #if defined(PG_VERSION_NUM) && (PG_VERSION_NUM < 140000) static bool CreateTableAsRelExists(CreateTableAsStmt *ctas); #endif /* - * ExecCreateImmv -- execute a create_immv() function + * create_immv_internal * - * This imitates PostgreSQL's ExecCreateTableAs(). + * Internal utility used for the creation of the definition of an IMMV. + * Caller needs to provide a list of attributes (ColumnDef nodes). + * + * This imitates PostgreSQL's create_ctas_internal(). */ -ObjectAddress -ExecCreateImmv(ParseState *pstate, CreateTableAsStmt *stmt, - ParamListInfo params, QueryEnvironment *queryEnv, - QueryCompletion *qc) +static ObjectAddress +create_immv_internal(List *attrList, IntoClause *into) { - Query *query = castNode(Query, stmt->query); - IntoClause *into = stmt->into; - bool is_matview = (into->viewQuery != NULL); - DestReceiver *dest; - Oid save_userid = InvalidOid; - int save_sec_context = 0; - int save_nestlevel = 0; - ObjectAddress address; - List *rewritten; - PlannedStmt *plan; - QueryDesc *queryDesc; - Query *viewQuery = (Query *) into->viewQuery; + CreateStmt *create = makeNode(CreateStmt); + char relkind; + Datum toast_options; + static char *validnsps[] = HEAP_RELOPT_NAMESPACES; + ObjectAddress intoRelationAddr; - /* - * We use this always true flag to imitate ExecCreaetTableAs(9 - * aiming to make it easier to follow up the original code. - */ - const bool is_ivm = true; - - /* must be a CREATE MATERIALIZED VIEW statement */ - Assert(is_matview); + /* This code supports both CREATE TABLE AS and CREATE MATERIALIZED VIEW */ + /* relkind of IMMV must be RELKIND_RELATION */ + relkind = RELKIND_RELATION; /* - * Set into->viewQuery must to NULL because we want to make a - * table instead of a materialized view. Before that, save the - * view query. + * Create the target relation by faking up a CREATE TABLE parsetree and + * passing it to DefineRelation. */ - viewQuery = (Query *) into->viewQuery; - into->viewQuery = NULL; - - /* Check if the relation exists or not */ - if (CreateTableAsRelExists(stmt)) - return InvalidObjectAddress; + create->relation = into->rel; + create->tableElts = attrList; + create->inhRelations = NIL; + create->ofTypename = NULL; + create->constraints = NIL; + create->options = into->options; + create->oncommit = into->onCommit; + create->tablespacename = into->tableSpaceName; + create->if_not_exists = false; + create->accessMethod = into->accessMethod; /* - * Create the tuple receiver object and insert info it will need + * Create the relation. (This will error out if there's an existing view, + * so we don't need more code to complain if "replace" is false.) */ - dest = CreateIntoRelDestReceiver(into); + intoRelationAddr = DefineRelation(create, relkind, InvalidOid, NULL, NULL); /* - * The contained Query must be a SELECT. + * If necessary, create a TOAST table for the target table. Note that + * NewRelationCreateToastTable ends with CommandCounterIncrement(), so + * that the TOAST table will be visible for insertion. */ - Assert(query->commandType == CMD_SELECT); + CommandCounterIncrement(); - /* - * For materialized views, lock down security-restricted operations and - * arrange to make GUC variable changes local to this command. This is - * not necessary for security, but this keeps the behavior similar to - * REFRESH MATERIALIZED VIEW. Otherwise, one could create a materialized - * view not possible to refresh. - */ - if (is_matview) - { - GetUserIdAndSecContext(&save_userid, &save_sec_context); - SetUserIdAndSecContext(save_userid, - save_sec_context | SECURITY_RESTRICTED_OPERATION); - save_nestlevel = NewGUCNestLevel(); - } + /* parse and validate reloptions for the toast table */ + toast_options = transformRelOptions((Datum) 0, + create->options, + "toast", + validnsps, + true, false); - if (is_matview && is_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"))); + (void) heap_reloptions(RELKIND_TOASTVALUE, toast_options, true); - check_ivm_restriction((Node *) query); + NewRelationCreateToastTable(intoRelationAddr.objectId, toast_options); - /* For IMMV, we need to rewrite matview query */ - query = rewriteQueryForIMMV(viewQuery, into->colNames); + /* Create the "view" part of an IMMV. */ + StoreImmvQuery(intoRelationAddr.objectId, (Query *) into->viewQuery); + CommandCounterIncrement(); - } + return intoRelationAddr; +} - if (into->skipData) - { - /* - * If WITH NO DATA was specified, do not go through the rewriter, - * planner and executor. Just define the relation using a code path - * similar to CREATE VIEW. This avoids dump/restore problems stemming - * from running the planner before all dependencies are set up. - */ +/* + * create_immv_nodata + * + * Create an IMMV when WITH NO DATA is used, starting from + * the targetlist of the view definition. + * + * This imitates PostgreSQL's create_ctas_nodata(). + */ +static ObjectAddress +create_immv_nodata(List *tlist, IntoClause *into) +{ + List *attrList; + ListCell *t, + *lc; - /* XXX: Currently, WITH NO DATA is not supported in the extension version */ - //address = create_ctas_nodata(query->targetList, into); - } - else + /* + * Build list of ColumnDefs from non-junk elements of the tlist. If a + * column name list was specified in CREATE TABLE AS, override the column + * names in the query. (Too few column names are OK, too many are not.) + */ + attrList = NIL; + lc = list_head(into->colNames); + foreach(t, tlist) { - /* - * Parse analysis was done already, but we still have to run the rule - * rewriter. We do not do AcquireRewriteLocks: we assume the query - * either came straight from the parser, or suitable locks were - * acquired by plancache.c. - */ - rewritten = QueryRewrite(query); + TargetEntry *tle = (TargetEntry *) lfirst(t); - /* SELECT should never rewrite to more or less than one SELECT query */ - if (list_length(rewritten) != 1) - elog(ERROR, "unexpected rewrite result for %s", - is_matview ? "CREATE MATERIALIZED VIEW" : - "CREATE TABLE AS SELECT"); - query = linitial_node(Query, rewritten); - Assert(query->commandType == CMD_SELECT); + if (!tle->resjunk) + { + ColumnDef *col; + char *colname; - /* plan the query */ - plan = pg_plan_query(query, pstate->p_sourcetext, - CURSOR_OPT_PARALLEL_OK, params); + if (lc) + { + colname = strVal(lfirst(lc)); + lc = lnext(into->colNames, lc); + } + else + colname = tle->resname; + + col = makeColumnDef(colname, + exprType((Node *) tle->expr), + exprTypmod((Node *) tle->expr), + exprCollation((Node *) tle->expr)); + + /* + * It's possible that the column is of a collatable type but the + * collation could not be resolved, so double-check. (We must + * check this here because DefineRelation would adopt the type's + * default collation rather than complaining.) + */ + if (!OidIsValid(col->collOid) && + type_is_collatable(col->typeName->typeOid)) + ereport(ERROR, + (errcode(ERRCODE_INDETERMINATE_COLLATION), + errmsg("no collation was derived for column \"%s\" with collatable type %s", + col->colname, + format_type_be(col->typeName->typeOid)), + errhint("Use the COLLATE clause to set the collation explicitly."))); - /* - * 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 - * parallel to the EXPLAIN code path.) - */ - PushCopiedSnapshot(GetActiveSnapshot()); - UpdateActiveSnapshotCommandId(); + attrList = lappend(attrList, col); + } + } - /* Create a QueryDesc, redirecting output to our tuple receiver */ - queryDesc = CreateQueryDesc(plan, pstate->p_sourcetext, - GetActiveSnapshot(), InvalidSnapshot, - dest, params, queryEnv, 0); + if (lc != NULL) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("too many column names were specified"))); - /* call ExecutorStart to prepare the plan for execution */ - ExecutorStart(queryDesc, GetIntoRelEFlags(into)); + /* Create the relation definition using the ColumnDef list */ + return create_immv_internal(attrList, into); +} - /* run the plan to completion */ - ExecutorRun(queryDesc, ForwardScanDirection, 0L, true); - /* save the rowcount if we're given a qc to fill */ - if (qc) - SetQueryCompletion(qc, CMDTAG_SELECT, queryDesc->estate->es_processed); +/* + * ExecCreateImmv -- execute a create_immv() function + * + * This imitates PostgreSQL's ExecCreateTableAs(). + */ +ObjectAddress +ExecCreateImmv(ParseState *pstate, CreateTableAsStmt *stmt, + ParamListInfo params, QueryEnvironment *queryEnv, + QueryCompletion *qc) +{ + Query *query = castNode(Query, stmt->query); + IntoClause *into = stmt->into; + bool do_refresh = false; + ObjectAddress address; + + /* Check if the relation exists or not */ + if (CreateTableAsRelExists(stmt)) + return InvalidObjectAddress; + + /* + * The contained Query must be a SELECT. + */ + Assert(query->commandType == CMD_SELECT); - /* get object address that intorel_startup saved for us */ - address = ((DR_intorel *) dest)->reladdr; + /* + * For materialized views, always skip data during table creation, and use + * REFRESH instead (see below). + */ + do_refresh = !into->skipData; - /* and clean up */ - ExecutorFinish(queryDesc); - ExecutorEnd(queryDesc); + /* 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"))); - FreeQueryDesc(queryDesc); + check_ivm_restriction((Node *) query); - PopActiveSnapshot(); - } + /* For IMMV, we need to rewrite matview query */ + query = rewriteQueryForIMMV(query, into->colNames); - /* Create the "view" part of an IMMV. */ - StoreImmvQuery(address.objectId, !into->skipData, viewQuery); + /* + * If WITH NO DATA was specified, do not go through the rewriter, + * planner and executor. Just define the relation using a code path + * similar to CREATE VIEW. This avoids dump/restore problems stemming + * from running the planner before all dependencies are set up. + */ + address = create_immv_nodata(query->targetList, into); - if (is_matview) + /* + * For materialized views, reuse the REFRESH logic, which locks down + * security-restricted operations and restricts the search_path. This + * reduces the chance that a subsequent refresh will fail. + */ + if (do_refresh) { - /* Roll back any GUC changes */ - AtEOXact_GUC(false, save_nestlevel); + Relation matviewRel; - /* Restore userid and security context */ - SetUserIdAndSecContext(save_userid, save_sec_context); + RefreshImmvByOid(address.objectId, false, pstate->p_sourcetext, qc); - if (is_ivm) - { - Oid matviewOid = address.objectId; - Relation matviewRel = table_open(matviewOid, NoLock); + if (qc) + qc->commandTag = CMDTAG_SELECT; - if (!into->skipData) - { - /* Create an index on incremental maintainable materialized view, if possible */ - CreateIndexOnIMMV(viewQuery, matviewRel); + matviewRel = table_open(address.objectId, NoLock); - /* - * Create triggers on incremental maintainable materialized view - * This argument should use 'query'. This needs to use a rewritten query, - * because a sublink in jointree is not supported by this function. - */ - CreateIvmTriggersOnBaseTables(query, matviewOid); + /* Create an index on incremental maintainable materialized view, if possible */ + CreateIndexOnIMMV(query, matviewRel); - /* Create triggers to prevent IMMV from beeing changed */ - CreateChangePreventTrigger(matviewOid); - } - table_close(matviewRel, NoLock); - } + /* Create triggers to prevent IMMV from beeing changed */ + CreateChangePreventTrigger(address.objectId); + + table_close(matviewRel, NoLock); } return address; @@ -1375,11 +1400,7 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) index->excludeOpNames = NIL; index->idxcomment = NULL; index->indexOid = InvalidOid; -#if defined(PG_VERSION_NUM) && (PG_VERSION_NUM >= 170000) - index->iswithoutoverlaps = false; - index->oldNumber = InvalidRelFileNumber; - index->oldFirstRelfilelocatorSubid = InvalidSubTransactionId; -#elif defined(PG_VERSION_NUM) && (PG_VERSION_NUM >= 160000) +#if defined(PG_VERSION_NUM) && (PG_VERSION_NUM >= 160000) index->oldNumber = InvalidRelFileNumber; index->oldFirstRelfilelocatorSubid = InvalidSubTransactionId; #else @@ -1486,18 +1507,10 @@ CreateIndexOnIMMV(Query *query, Relation matviewRel) indexRel = index_open(indexoid, AccessShareLock); -#if defined(PG_VERSION_NUM) && (PG_VERSION_NUM >= 170000) - if (CheckIndexCompatible(indexRel->rd_id, - index->accessMethod, - index->indexParams, - index->excludeOpNames, - index->iswithoutoverlaps)) -#else if (CheckIndexCompatible(indexRel->rd_id, index->accessMethod, index->indexParams, index->excludeOpNames)) -#endif hasCompatibleIndex = true; index_close(indexRel, AccessShareLock); @@ -1677,7 +1690,7 @@ get_primary_key_attnos_from_query(Query *query, List **constraintList) * Store the query for the IMMV to pg_ivwm_immv */ static void -StoreImmvQuery(Oid viewOid, bool ispopulated, Query *viewQuery) +StoreImmvQuery(Oid viewOid, Query *viewQuery) { char *querytree = nodeToString((Node *) viewQuery); Datum values[Natts_pg_ivm_immv]; @@ -1691,7 +1704,7 @@ StoreImmvQuery(Oid viewOid, bool ispopulated, Query *viewQuery) memset(isNulls, false, sizeof(isNulls)); values[Anum_pg_ivm_immv_immvrelid -1 ] = ObjectIdGetDatum(viewOid); - values[Anum_pg_ivm_immv_ispopulated -1 ] = BoolGetDatum(ispopulated); + values[Anum_pg_ivm_immv_ispopulated -1 ] = BoolGetDatum(false); values[Anum_pg_ivm_immv_viewdef -1 ] = CStringGetTextDatum(querytree); pgIvmImmv = table_open(PgIvmImmvRelationId(), RowExclusiveLock); diff --git a/matview.c b/matview.c index 16848c1..6b87c8f 100644 --- a/matview.c +++ b/matview.c @@ -234,6 +234,40 @@ ExecRefreshImmv(const RangeVar *relation, bool skipData, const char *queryString, QueryCompletion *qc) { Oid matviewOid; + LOCKMODE lockmode; + + /* Determine strength of lock needed. */ + //concurrent = stmt->concurrent; + //lockmode = concurrent ? ExclusiveLock : AccessExclusiveLock; + lockmode = AccessExclusiveLock; + + /* + * Get a lock until end of transaction. + */ +#if defined(PG_VERSION_NUM) && (PG_VERSION_NUM < 170000) + matviewOid = RangeVarGetRelidExtended(relation, + lockmode, 0, + RangeVarCallbackOwnsTable, + NULL); +#else + matviewOid = RangeVarGetRelidExtended(relation, + lockmode, 0, + RangeVarCallbackMaintainsTable, + NULL); +#endif + + return RefreshImmvByOid(matviewOid, skipData, queryString, qc); +} + +/* + * RefreshMatViewByOid -- refresh IMMV view by OID + * + * This imitates PostgreSQL's RefreshMatViewByOid(). + */ +ObjectAddress +RefreshImmvByOid(Oid matviewOid, bool skipData, + const char *queryString, QueryCompletion *qc) +{ Relation matviewRel; Query *dataQuery = NULL; /* initialized to keep compiler happy */ Query *viewQuery; @@ -242,8 +276,6 @@ ExecRefreshImmv(const RangeVar *relation, bool skipData, Oid OIDNewHeap; DestReceiver *dest; uint64 processed = 0; - //bool concurrent; - LOCKMODE lockmode; char relpersistence; Oid save_userid; int save_sec_context; @@ -259,18 +291,7 @@ ExecRefreshImmv(const RangeVar *relation, bool skipData, bool isnull; Datum datum; - /* Determine strength of lock needed. */ - //concurrent = stmt->concurrent; - //lockmode = concurrent ? ExclusiveLock : AccessExclusiveLock; - lockmode = AccessExclusiveLock; - - /* - * Get a lock until end of transaction. - */ - matviewOid = RangeVarGetRelidExtended(relation, - lockmode, 0, - RangeVarCallbackOwnsTable, NULL); - matviewRel = table_open(matviewOid, lockmode); + matviewRel = table_open(matviewOid, NoLock); relowner = matviewRel->rd_rel->relowner; /* @@ -282,8 +303,12 @@ ExecRefreshImmv(const RangeVar *relation, bool skipData, SetUserIdAndSecContext(relowner, save_sec_context | SECURITY_RESTRICTED_OPERATION); save_nestlevel = NewGUCNestLevel(); +#if defined(PG_VERSION_NUM) && (PG_VERSION_NUM >= 170000) + RestrictSearchPath(); +#endif /* + * Make sure it is an IMMV: * Get the entry in pg_ivm_immv. If it doesn't exist, the relation * is not IMMV. */ @@ -306,6 +331,15 @@ ExecRefreshImmv(const RangeVar *relation, bool skipData, Assert(!isnull); oldPopulated = DatumGetBool(datum); + /* + * 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 an IMMV"); + /* Tentatively mark the IMMV as populated or not (this will roll back * if we fail later). */ @@ -343,15 +377,6 @@ ExecRefreshImmv(const RangeVar *relation, bool skipData, if (!skipData) dataQuery = rewriteQueryForIMMV(viewQuery,NIL); - /* - * 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 an IMMV"); - tableSpace = matviewRel->rd_rel->reltablespace; relpersistence = matviewRel->rd_rel->relpersistence; @@ -452,8 +477,13 @@ ExecRefreshImmv(const RangeVar *relation, bool skipData, if (!skipData) pgstat_count_heap_insert(matviewRel, processed); + /* + * Create triggers on incremental maintainable materialized view + * This argument should use 'dataQuery'. This needs to use a rewritten query, + * because a sublink in jointree is not supported by this function. + */ if (!skipData && !oldPopulated) - CreateIvmTriggersOnBaseTables(viewQuery, matviewOid); + CreateIvmTriggersOnBaseTables(dataQuery, matviewOid); table_close(matviewRel, NoLock); @@ -534,7 +564,7 @@ refresh_immv_datafill(DestReceiver *dest, Query *query, ExecutorStart(queryDesc, 0); /* run the plan */ - ExecutorRun(queryDesc, ForwardScanDirection, 0L, true); + ExecutorRun(queryDesc, ForwardScanDirection, 0, true); processed = queryDesc->estate->es_processed; @@ -889,6 +919,9 @@ IVM_immediate_maintenance(PG_FUNCTION_ARGS) SetUserIdAndSecContext(relowner, save_sec_context | SECURITY_RESTRICTED_OPERATION); save_nestlevel = NewGUCNestLevel(); +#if defined(PG_VERSION_NUM) && (PG_VERSION_NUM >= 170000) + RestrictSearchPath(); +#endif /* get view query*/ query = get_immv_query(matviewRel); diff --git a/pg_ivm.h b/pg_ivm.h index 7662bea..2c76f9b 100644 --- a/pg_ivm.h +++ b/pg_ivm.h @@ -48,6 +48,8 @@ extern void makeIvmAggColumn(ParseState *pstate, Aggref *aggref, char *resname, extern Query *get_immv_query(Relation matviewRel); extern ObjectAddress ExecRefreshImmv(const RangeVar *relation, bool skipData, const char *queryString, QueryCompletion *qc); +extern ObjectAddress RefreshImmvByOid(Oid matviewOid, bool skipData, + const char *queryString, QueryCompletion *qc); extern bool ImmvIncrementalMaintenanceIsEnabled(void); extern Query *get_immv_query(Relation matviewRel); extern Datum IVM_immediate_before(PG_FUNCTION_ARGS);