When creating materialized views, use REFRESH to load data.
authorJeff Davis <jdavis@postgresql.org>
Tue, 16 Jul 2024 22:41:29 +0000 (15:41 -0700)
committerJeff Davis <jdavis@postgresql.org>
Tue, 16 Jul 2024 22:41:29 +0000 (15:41 -0700)
Previously, CREATE MATERIALIZED VIEW ... WITH DATA populated the MV
the same way as CREATE TABLE ... AS.

Instead, 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.

Reported-by: Noah Misch
Backpatch-through: 17
Discussion: https://postgr.es/m/20240630222344.db.nmisch@google.com

src/backend/commands/createas.c
src/backend/commands/matview.c
src/include/commands/matview.h
src/test/regress/expected/namespace.out

index 62050f4dc590b8a17326e64405562e91cc4923b9..2c8a93b6e56f86b0c379e3c89eacc2354e7ea3bd 100644 (file)
@@ -225,10 +225,8 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt,
        Query      *query = castNode(Query, stmt->query);
        IntoClause *into = stmt->into;
        bool            is_matview = (into->viewQuery != NULL);
+       bool            do_refresh = false;
        DestReceiver *dest;
-       Oid                     save_userid = InvalidOid;
-       int                     save_sec_context = 0;
-       int                     save_nestlevel = 0;
        ObjectAddress address;
        List       *rewritten;
        PlannedStmt *plan;
@@ -263,18 +261,13 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt,
        Assert(query->commandType == CMD_SELECT);
 
        /*
-        * 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.
+        * For materialized views, always skip data during table creation, and use
+        * REFRESH instead (see below).
         */
        if (is_matview)
        {
-               GetUserIdAndSecContext(&save_userid, &save_sec_context);
-               SetUserIdAndSecContext(save_userid,
-                                                          save_sec_context | SECURITY_RESTRICTED_OPERATION);
-               save_nestlevel = NewGUCNestLevel();
+               do_refresh = !into->skipData;
+               into->skipData = true;
        }
 
        if (into->skipData)
@@ -346,13 +339,18 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt,
                PopActiveSnapshot();
        }
 
-       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);
+               RefreshMatViewByOid(address.objectId, false, false,
+                                                       pstate->p_sourcetext, NULL, qc);
 
-               /* Restore userid and security context */
-               SetUserIdAndSecContext(save_userid, save_sec_context);
+               if (qc)
+                       qc->commandTag = CMDTAG_SELECT;
        }
 
        return address;
index ea05d4b224fa857692a7547efeb212351179ae02..84245b65f7dad8b67629d4e599b0b30f6d0b3166 100644 (file)
@@ -112,15 +112,44 @@ SetMatViewPopulatedState(Relation relation, bool newstate)
 /*
  * ExecRefreshMatView -- execute a REFRESH MATERIALIZED VIEW command
  *
+ * If WITH NO DATA was specified, this is effectively like a TRUNCATE;
+ * otherwise it is like a TRUNCATE followed by an INSERT using the SELECT
+ * statement associated with the materialized view.  The statement node's
+ * skipData field shows whether the clause was used.
+ */
+ObjectAddress
+ExecRefreshMatView(RefreshMatViewStmt *stmt, const char *queryString,
+                                  ParamListInfo params, QueryCompletion *qc)
+{
+       Oid                     matviewOid;
+       LOCKMODE        lockmode;
+
+       /* Determine strength of lock needed. */
+       lockmode = stmt->concurrent ? ExclusiveLock : AccessExclusiveLock;
+
+       /*
+        * Get a lock until end of transaction.
+        */
+       matviewOid = RangeVarGetRelidExtended(stmt->relation,
+                                                                                 lockmode, 0,
+                                                                                 RangeVarCallbackMaintainsTable,
+                                                                                 NULL);
+
+       return RefreshMatViewByOid(matviewOid, stmt->skipData, stmt->concurrent,
+                                                          queryString, params, qc);
+}
+
+/*
+ * RefreshMatViewByOid -- refresh materialized view by OID
+ *
  * This refreshes the materialized view by creating a new table and swapping
  * the relfilenumbers of the new table and the old materialized view, so the OID
  * of the original materialized view is preserved. Thus we do not lose GRANT
  * nor references to this materialized view.
  *
- * If WITH NO DATA was specified, this is effectively like a TRUNCATE;
- * otherwise it is like a TRUNCATE followed by an INSERT using the SELECT
- * statement associated with the materialized view.  The statement node's
- * skipData field shows whether the clause was used.
+ * If skipData is true, this is effectively like a TRUNCATE; otherwise it is
+ * like a TRUNCATE followed by an INSERT using the SELECT statement associated
+ * with the materialized view.
  *
  * Indexes are rebuilt too, via REINDEX. Since we are effectively bulk-loading
  * the new heap, it's better to create the indexes afterwards than to fill them
@@ -130,10 +159,10 @@ SetMatViewPopulatedState(Relation relation, bool newstate)
  * reflect the result set of the materialized view's query.
  */
 ObjectAddress
-ExecRefreshMatView(RefreshMatViewStmt *stmt, const char *queryString,
-                                  ParamListInfo params, QueryCompletion *qc)
+RefreshMatViewByOid(Oid matviewOid, bool skipData, bool concurrent,
+                                       const char *queryString, ParamListInfo params,
+                                       QueryCompletion *qc)
 {
-       Oid                     matviewOid;
        Relation        matviewRel;
        RewriteRule *rule;
        List       *actions;
@@ -143,25 +172,12 @@ ExecRefreshMatView(RefreshMatViewStmt *stmt, const char *queryString,
        Oid                     OIDNewHeap;
        DestReceiver *dest;
        uint64          processed = 0;
-       bool            concurrent;
-       LOCKMODE        lockmode;
        char            relpersistence;
        Oid                     save_userid;
        int                     save_sec_context;
        int                     save_nestlevel;
        ObjectAddress address;
 
-       /* Determine strength of lock needed. */
-       concurrent = stmt->concurrent;
-       lockmode = concurrent ? ExclusiveLock : AccessExclusiveLock;
-
-       /*
-        * Get a lock until end of transaction.
-        */
-       matviewOid = RangeVarGetRelidExtended(stmt->relation,
-                                                                                 lockmode, 0,
-                                                                                 RangeVarCallbackMaintainsTable,
-                                                                                 NULL);
        matviewRel = table_open(matviewOid, NoLock);
        relowner = matviewRel->rd_rel->relowner;
 
@@ -190,7 +206,7 @@ ExecRefreshMatView(RefreshMatViewStmt *stmt, const char *queryString,
                                 errmsg("CONCURRENTLY cannot be used when the materialized view is not populated")));
 
        /* Check that conflicting options have not been specified. */
-       if (concurrent && stmt->skipData)
+       if (concurrent && skipData)
                ereport(ERROR,
                                (errcode(ERRCODE_SYNTAX_ERROR),
                                 errmsg("%s and %s options cannot be used together",
@@ -275,7 +291,7 @@ ExecRefreshMatView(RefreshMatViewStmt *stmt, const char *queryString,
         * Tentatively mark the matview as populated or not (this will roll back
         * if we fail later).
         */
-       SetMatViewPopulatedState(matviewRel, !stmt->skipData);
+       SetMatViewPopulatedState(matviewRel, !skipData);
 
        /* Concurrent refresh builds new data in temp tablespace, and does diff. */
        if (concurrent)
@@ -301,7 +317,7 @@ ExecRefreshMatView(RefreshMatViewStmt *stmt, const char *queryString,
        dest = CreateTransientRelDestReceiver(OIDNewHeap);
 
        /* Generate the data, if wanted. */
-       if (!stmt->skipData)
+       if (!skipData)
                processed = refresh_matview_datafill(dest, dataQuery, queryString);
 
        /* Make the matview match the newly generated data. */
@@ -333,7 +349,7 @@ ExecRefreshMatView(RefreshMatViewStmt *stmt, const char *queryString,
                 * inserts and deletes it issues get counted by lower-level code.)
                 */
                pgstat_count_truncate(matviewRel);
-               if (!stmt->skipData)
+               if (!skipData)
                        pgstat_count_heap_insert(matviewRel, processed);
        }
 
index 817b2ba0b6fc49c5287ff609e786b96b8e52b1db..a226b2e68fba5f1de74279ba9a424e0940469ad1 100644 (file)
@@ -25,6 +25,9 @@ extern void SetMatViewPopulatedState(Relation relation, bool newstate);
 
 extern ObjectAddress ExecRefreshMatView(RefreshMatViewStmt *stmt, const char *queryString,
                                                                                ParamListInfo params, QueryCompletion *qc);
+extern ObjectAddress RefreshMatViewByOid(Oid matviewOid, bool skipData, bool concurrent,
+                                                                                const char *queryString, ParamListInfo params,
+                                                                                QueryCompletion *qc);
 
 extern DestReceiver *CreateTransientRelDestReceiver(Oid transientoid);
 
index 7d36e9cc0c4b2d09678e28e3ea491cbc293c4c42..dbbda72d3951af6f67e4c7d7c355e7e934d9d1d5 100644 (file)
@@ -129,8 +129,8 @@ $$;
 CREATE TABLE test_maint(i INT);
 INSERT INTO test_maint VALUES (1), (2);
 CREATE MATERIALIZED VIEW test_maint_mv AS SELECT fn(i) FROM test_maint;
-NOTICE:  current search_path: test_maint_search_path
-NOTICE:  current search_path: test_maint_search_path
+NOTICE:  current search_path: pg_catalog, pg_temp
+NOTICE:  current search_path: pg_catalog, pg_temp
 -- the following commands should see search_path as pg_catalog, pg_temp
 CREATE INDEX test_maint_idx ON test_maint_search_path.test_maint (fn(i));
 NOTICE:  current search_path: pg_catalog, pg_temp