Add support for SET ACCESS METHOD in ALTER TABLE
authorMichael Paquier <michael@paquier.xyz>
Wed, 28 Jul 2021 01:10:44 +0000 (10:10 +0900)
committerMichael Paquier <michael@paquier.xyz>
Wed, 28 Jul 2021 01:10:44 +0000 (10:10 +0900)
The logic used to support a change of access method for a table is
similar to changes for tablespace or relation persistence, requiring a
table rewrite with an exclusive lock of the relation changed.  Table
rewrites done in ALTER TABLE already go through the table AM layer when
scanning tuples from the old relation and inserting them into the new
one, making this implementation straight-forward.

Note that partitioned tables are not supported as these have no access
methods defined.

Author: Justin Pryzby, Jeff Davis
Reviewed-by: Michael Paquier, Vignesh C
Discussion: https://postgr.es/m/20210228222530.GD20769@telsasoft.com

doc/src/sgml/ref/alter_table.sgml
src/backend/commands/cluster.c
src/backend/commands/matview.c
src/backend/commands/tablecmds.c
src/backend/parser/gram.y
src/bin/psql/tab-complete.c
src/include/commands/cluster.h
src/include/commands/event_trigger.h
src/include/nodes/parsenodes.h
src/test/regress/expected/create_am.out
src/test/regress/sql/create_am.sql

index 1c7f48938bc6783deaa8771c94ddecec3d816e5e..bf90040aa22a8846bf146ca1d95795e3195a8313 100644 (file)
@@ -75,6 +75,7 @@ ALTER TABLE [ IF EXISTS ] <replaceable class="parameter">name</replaceable>
     CLUSTER ON <replaceable class="parameter">index_name</replaceable>
     SET WITHOUT CLUSTER
     SET WITHOUT OIDS
+    SET ACCESS METHOD <replaceable class="parameter">new_access_method</replaceable>
     SET TABLESPACE <replaceable class="parameter">new_tablespace</replaceable>
     SET { LOGGED | UNLOGGED }
     SET ( <replaceable class="parameter">storage_parameter</replaceable> [= <replaceable class="parameter">value</replaceable>] [, ... ] )
@@ -692,6 +693,16 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
     </listitem>
    </varlistentry>
 
+   <varlistentry>
+    <term><literal>SET ACCESS METHOD</literal></term>
+    <listitem>
+     <para>
+      This form changes the access method of the table by rewriting it. See
+      <xref linkend="tableam"/> for more information.
+     </para>
+    </listitem>
+   </varlistentry>
+
    <varlistentry>
     <term><literal>SET TABLESPACE</literal></term>
     <listitem>
@@ -1228,6 +1239,15 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><replaceable class="parameter">new_access_method</replaceable></term>
+      <listitem>
+       <para>
+        The name of the access method to which the table will be converted.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><replaceable class="parameter">new_tablespace</replaceable></term>
       <listitem>
index 6487a9e3fcb9cf250c7399ec01c6cac83ebd8638..b3d8b6deb036bfdb856f025909197b11863d3ba4 100644 (file)
@@ -576,6 +576,7 @@ static void
 rebuild_relation(Relation OldHeap, Oid indexOid, bool verbose)
 {
        Oid                     tableOid = RelationGetRelid(OldHeap);
+       Oid                     accessMethod = OldHeap->rd_rel->relam;
        Oid                     tableSpace = OldHeap->rd_rel->reltablespace;
        Oid                     OIDNewHeap;
        char            relpersistence;
@@ -597,6 +598,7 @@ rebuild_relation(Relation OldHeap, Oid indexOid, bool verbose)
 
        /* Create the transient table that will receive the re-ordered data */
        OIDNewHeap = make_new_heap(tableOid, tableSpace,
+                                                          accessMethod,
                                                           relpersistence,
                                                           AccessExclusiveLock);
 
@@ -618,16 +620,16 @@ rebuild_relation(Relation OldHeap, Oid indexOid, bool verbose)
 /*
  * Create the transient table that will be filled with new data during
  * CLUSTER, ALTER TABLE, and similar operations.  The transient table
- * duplicates the logical structure of the OldHeap, but is placed in
- * NewTableSpace which might be different from OldHeap's.  Also, it's built
- * with the specified persistence, which might differ from the original's.
+ * duplicates the logical structure of the OldHeap; but will have the
+ * specified physical storage properties NewTableSpace, NewAccessMethod, and
+ * relpersistence.
  *
  * After this, the caller should load the new heap with transferred/modified
  * data, then call finish_heap_swap to complete the operation.
  */
 Oid
-make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, char relpersistence,
-                         LOCKMODE lockmode)
+make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
+                         char relpersistence, LOCKMODE lockmode)
 {
        TupleDesc       OldHeapDesc;
        char            NewHeapName[NAMEDATALEN];
@@ -686,7 +688,7 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, char relpersistence,
                                                                                  InvalidOid,
                                                                                  InvalidOid,
                                                                                  OldHeap->rd_rel->relowner,
-                                                                                 OldHeap->rd_rel->relam,
+                                                                                 NewAccessMethod,
                                                                                  OldHeapDesc,
                                                                                  NIL,
                                                                                  RELKIND_RELATION,
@@ -1036,6 +1038,10 @@ swap_relation_files(Oid r1, Oid r2, bool target_is_pg_class,
                relform1->reltablespace = relform2->reltablespace;
                relform2->reltablespace = swaptemp;
 
+               swaptemp = relform1->relam;
+               relform1->relam = relform2->relam;
+               relform2->relam = swaptemp;
+
                swptmpchr = relform1->relpersistence;
                relform1->relpersistence = relform2->relpersistence;
                relform2->relpersistence = swptmpchr;
@@ -1071,6 +1077,9 @@ swap_relation_files(Oid r1, Oid r2, bool target_is_pg_class,
                if (relform1->relpersistence != relform2->relpersistence)
                        elog(ERROR, "cannot change persistence of mapped relation \"%s\"",
                                 NameStr(relform1->relname));
+               if (relform1->relam != relform2->relam)
+                       elog(ERROR, "cannot change access method of mapped relation \"%s\"",
+                                NameStr(relform1->relname));
                if (!swap_toast_by_content &&
                        (relform1->reltoastrelid || relform2->reltoastrelid))
                        elog(ERROR, "cannot swap toast by links for mapped relation \"%s\"",
index 25bbd8a5c14dec39b7aa3545c40c53fd275514bf..9493b227b4b17b55d87a2d5ee4bc67262904268f 100644 (file)
@@ -298,8 +298,9 @@ ExecRefreshMatView(RefreshMatViewStmt *stmt, const char *queryString,
         * it against access by any other process until commit (by which time it
         * will be gone).
         */
-       OIDNewHeap = make_new_heap(matviewOid, tableSpace, relpersistence,
-                                                          ExclusiveLock);
+       OIDNewHeap = make_new_heap(matviewOid, tableSpace,
+                                                          matviewRel->rd_rel->relam,
+                                                          relpersistence, ExclusiveLock);
        LockRelationOid(OIDNewHeap, AccessExclusiveLock);
        dest = CreateTransientRelDestReceiver(OIDNewHeap);
 
index a16e7495067b4c141a102d28b7ccf6f9f9121e7d..fcd778c62a5187f24cad758919a64d117783606c 100644 (file)
@@ -176,6 +176,7 @@ typedef struct AlteredTableInfo
        List       *afterStmts;         /* List of utility command parsetrees */
        bool            verify_new_notnull; /* T if we should recheck NOT NULL */
        int                     rewrite;                /* Reason for forced rewrite, if any */
+       Oid                     newAccessMethod;        /* new access method; 0 means no change */
        Oid                     newTableSpace;  /* new tablespace; 0 means no change */
        bool            chgPersistence; /* T if SET LOGGED/UNLOGGED is used */
        char            newrelpersistence;      /* if above is true */
@@ -538,6 +539,7 @@ static void change_owner_recurse_to_sequences(Oid relationOid,
 static ObjectAddress ATExecClusterOn(Relation rel, const char *indexName,
                                                                         LOCKMODE lockmode);
 static void ATExecDropCluster(Relation rel, LOCKMODE lockmode);
+static void ATPrepSetAccessMethod(AlteredTableInfo *tab, Relation rel, const char *amname);
 static bool ATPrepChangePersistence(Relation rel, bool toLogged);
 static void ATPrepSetTableSpace(AlteredTableInfo *tab, Relation rel,
                                                                const char *tablespacename, LOCKMODE lockmode);
@@ -4096,6 +4098,7 @@ AlterTableGetLockLevel(List *cmds)
                                 */
                        case AT_AddColumn:      /* may rewrite heap, in some cases and visible
                                                                 * to SELECT */
+                       case AT_SetAccessMethod:        /* must rewrite heap */
                        case AT_SetTableSpace:  /* must rewrite heap */
                        case AT_AlterColumnType:        /* must rewrite heap */
                                cmd_lockmode = AccessExclusiveLock;
@@ -4622,6 +4625,24 @@ ATPrepCmd(List **wqueue, Relation rel, AlterTableCmd *cmd,
                        ATSimplePermissions(cmd->subtype, rel, ATT_TABLE | ATT_FOREIGN_TABLE);
                        pass = AT_PASS_DROP;
                        break;
+               case AT_SetAccessMethod:        /* SET ACCESS METHOD */
+                       ATSimplePermissions(cmd->subtype, rel, ATT_TABLE | ATT_MATVIEW);
+
+                       /* partitioned tables don't have an access method */
+                       if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
+                               ereport(ERROR,
+                                               (errcode(ERRCODE_WRONG_OBJECT_TYPE),
+                                                errmsg("cannot change access method of a partitioned table")));
+
+                       /* check if another access method change was already requested */
+                       if (OidIsValid(tab->newAccessMethod))
+                               ereport(ERROR,
+                                               (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+                                                errmsg("cannot have multiple SET ACCESS METHOD subcommands")));
+
+                       ATPrepSetAccessMethod(tab, rel, cmd->name);
+                       pass = AT_PASS_MISC;    /* does not matter; no work in Phase 2 */
+                       break;
                case AT_SetTableSpace:  /* SET TABLESPACE */
                        ATSimplePermissions(cmd->subtype, rel, ATT_TABLE | ATT_MATVIEW | ATT_INDEX |
                                                                ATT_PARTITIONED_INDEX);
@@ -4997,6 +5018,9 @@ ATExecCmd(List **wqueue, AlteredTableInfo *tab,
                case AT_DropOids:               /* SET WITHOUT OIDS */
                        /* nothing to do here, oid columns don't exist anymore */
                        break;
+               case AT_SetAccessMethod:        /* SET ACCESS METHOD */
+                       /* handled specially in Phase 3 */
+                       break;
                case AT_SetTableSpace:  /* SET TABLESPACE */
 
                        /*
@@ -5324,7 +5348,7 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode,
 
                /*
                 * We only need to rewrite the table if at least one column needs to
-                * be recomputed, or we are changing its persistence.
+                * be recomputed, or we are changing its persistence or access method.
                 *
                 * There are two reasons for requiring a rewrite when changing
                 * persistence: on one hand, we need to ensure that the buffers
@@ -5338,6 +5362,7 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode,
                        /* Build a temporary relation and copy data */
                        Relation        OldHeap;
                        Oid                     OIDNewHeap;
+                       Oid                     NewAccessMethod;
                        Oid                     NewTableSpace;
                        char            persistence;
 
@@ -5378,6 +5403,15 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode,
                        else
                                NewTableSpace = OldHeap->rd_rel->reltablespace;
 
+                       /*
+                        * Select destination access method (same as original unless user
+                        * requested a change)
+                        */
+                       if (OidIsValid(tab->newAccessMethod))
+                               NewAccessMethod = tab->newAccessMethod;
+                       else
+                               NewAccessMethod = OldHeap->rd_rel->relam;
+
                        /*
                         * Select persistence of transient table (same as original unless
                         * user requested a change)
@@ -5417,8 +5451,8 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode,
                         * persistence. That wouldn't work for pg_class, but that can't be
                         * unlogged anyway.
                         */
-                       OIDNewHeap = make_new_heap(tab->relid, NewTableSpace, persistence,
-                                                                          lockmode);
+                       OIDNewHeap = make_new_heap(tab->relid, NewTableSpace, NewAccessMethod,
+                                                                          persistence, lockmode);
 
                        /*
                         * Copy the heap data into the new table with the desired
@@ -5933,6 +5967,8 @@ ATGetQueueEntry(List **wqueue, Relation rel)
        tab->rel = NULL;                        /* set later */
        tab->relkind = rel->rd_rel->relkind;
        tab->oldDesc = CreateTupleDescCopyConstr(RelationGetDescr(rel));
+       tab->newAccessMethod = InvalidOid;
+       tab->newTableSpace = InvalidOid;
        tab->newrelpersistence = RELPERSISTENCE_PERMANENT;
        tab->chgPersistence = false;
 
@@ -6003,6 +6039,8 @@ alter_table_type_to_string(AlterTableType cmdtype)
                        return "CLUSTER ON";
                case AT_DropCluster:
                        return "SET WITHOUT CLUSTER";
+               case AT_SetAccessMethod:
+                       return "SET ACCESS METHOD";
                case AT_SetLogged:
                        return "SET LOGGED";
                case AT_SetUnLogged:
@@ -13609,6 +13647,28 @@ ATExecDropCluster(Relation rel, LOCKMODE lockmode)
        mark_index_clustered(rel, InvalidOid, false);
 }
 
+/*
+ * Preparation phase for SET ACCESS METHOD
+ *
+ * Check that access method exists.  If it is the same as the table's current
+ * access method, it is a no-op.  Otherwise, a table rewrite is necessary.
+ */
+static void
+ATPrepSetAccessMethod(AlteredTableInfo *tab, Relation rel, const char *amname)
+{
+       Oid                     amoid;
+
+       /* Check that the table access method exists */
+       amoid = get_table_am_oid(amname, false);
+
+       if (rel->rd_rel->relam == amoid)
+               return;
+
+       /* Save info for Phase 3 to do the real work */
+       tab->rewrite |= AT_REWRITE_ACCESS_METHOD;
+       tab->newAccessMethod = amoid;
+}
+
 /*
  * ALTER TABLE SET TABLESPACE
  */
index 10da5c5c51df6db7b82aeac1365a36263a74e8bf..39a2849eba3a87403a7f755a750b1103b1d60b66 100644 (file)
@@ -2623,6 +2623,14 @@ alter_table_cmd:
                                        n->newowner = $3;
                                        $$ = (Node *)n;
                                }
+                       /* ALTER TABLE <name> SET ACCESS METHOD <amname> */
+                       | SET ACCESS METHOD name
+                               {
+                                       AlterTableCmd *n = makeNode(AlterTableCmd);
+                                       n->subtype = AT_SetAccessMethod;
+                                       n->name = $4;
+                                       $$ = (Node *)n;
+                               }
                        /* ALTER TABLE <name> SET TABLESPACE <tablespacename> */
                        | SET TABLESPACE name
                                {
index d6bf725971cdf2d92a385c13c420d53c10119728..cc13fc05bba0b2e5f15f1a3d643682e2b861c7fe 100644 (file)
@@ -2141,8 +2141,15 @@ psql_completion(const char *text, int start, int end)
        }
        /* If we have ALTER TABLE <sth> SET, provide list of attributes and '(' */
        else if (Matches("ALTER", "TABLE", MatchAny, "SET"))
-               COMPLETE_WITH("(", "LOGGED", "SCHEMA", "TABLESPACE", "UNLOGGED",
-                                         "WITH", "WITHOUT");
+               COMPLETE_WITH("(", "ACCESS METHOD", "LOGGED", "SCHEMA",
+                                         "TABLESPACE", "UNLOGGED", "WITH", "WITHOUT");
+
+       /*
+        * If we have ALTER TABLE <smt> SET ACCESS METHOD provide a list of table
+        * AMs.
+        */
+       else if (Matches("ALTER", "TABLE", MatchAny, "SET", "ACCESS", "METHOD"))
+               COMPLETE_WITH_QUERY(Query_for_list_of_table_access_methods);
 
        /*
         * If we have ALTER TABLE <sth> SET TABLESPACE provide a list of
index a941f2accdaed5ac7dbec463074f4d07562f462b..b64d3bc2040b683b0d112a443a199ecd8763c400 100644 (file)
@@ -35,8 +35,8 @@ extern void check_index_is_clusterable(Relation OldHeap, Oid indexOid,
                                                                           bool recheck, LOCKMODE lockmode);
 extern void mark_index_clustered(Relation rel, Oid indexOid, bool is_internal);
 
-extern Oid     make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, char relpersistence,
-                                                 LOCKMODE lockmode);
+extern Oid     make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
+                                                 char relpersistence, LOCKMODE lockmode);
 extern void finish_heap_swap(Oid OIDOldHeap, Oid OIDNewHeap,
                                                         bool is_system_catalog,
                                                         bool swap_toast_by_content,
index c11bf2d781094888a80df28e52cbc6795fcf5a54..e765e67fd103c38cfd1cd8f5953db6ac60c46b3c 100644 (file)
@@ -32,6 +32,7 @@ typedef struct EventTriggerData
 #define AT_REWRITE_ALTER_PERSISTENCE   0x01
 #define AT_REWRITE_DEFAULT_VAL                 0x02
 #define AT_REWRITE_COLUMN_REWRITE              0x04
+#define AT_REWRITE_ACCESS_METHOD               0x08
 
 /*
  * EventTriggerData is the node type that is passed as fmgr "context" info
index 947660a4b0196c19b42efea7a3866209c6070110..e28248af32421808ae6bb91cf8cd09ce25e2f8a2 100644 (file)
@@ -1901,6 +1901,7 @@ typedef enum AlterTableType
        AT_SetLogged,                           /* SET LOGGED */
        AT_SetUnLogged,                         /* SET UNLOGGED */
        AT_DropOids,                            /* SET WITHOUT OIDS */
+       AT_SetAccessMethod,                     /* SET ACCESS METHOD */
        AT_SetTableSpace,                       /* SET TABLESPACE */
        AT_SetRelOptions,                       /* SET (...) -- AM specific parameters */
        AT_ResetRelOptions,                     /* RESET (...) -- AM specific parameters */
index 0dfb26c30143d901121627ec82fa5b3954fa7bfa..32b7134080c305f0918326b47e54dea182899c36 100644 (file)
@@ -230,6 +230,40 @@ ORDER BY classid, objid, objsubid;
  table tableam_parted_d_heap2
 (5 rows)
 
+-- ALTER TABLE SET ACCESS METHOD
+CREATE TABLE heaptable USING heap AS
+  SELECT a, repeat(a::text, 100) FROM generate_series(1,9) AS a;
+SELECT amname FROM pg_class c, pg_am am
+  WHERE c.relam = am.oid AND c.oid = 'heaptable'::regclass;
+ amname 
+--------
+ heap
+(1 row)
+
+ALTER TABLE heaptable SET ACCESS METHOD heap2;
+SELECT amname FROM pg_class c, pg_am am
+  WHERE c.relam = am.oid AND c.oid = 'heaptable'::regclass;
+ amname 
+--------
+ heap2
+(1 row)
+
+SELECT COUNT(a), COUNT(1) FILTER(WHERE a=1) FROM heaptable;
+ count | count 
+-------+-------
+     9 |     1
+(1 row)
+
+-- No support for multiple subcommands
+ALTER TABLE heaptable SET ACCESS METHOD heap, SET ACCESS METHOD heap2;
+ERROR:  cannot have multiple SET ACCESS METHOD subcommands
+DROP TABLE heaptable;
+-- No support for partitioned tables.
+CREATE TABLE am_partitioned(x INT, y INT)
+  PARTITION BY hash (x);
+ALTER TABLE am_partitioned SET ACCESS METHOD heap2;
+ERROR:  cannot change access method of a partitioned table
+DROP TABLE am_partitioned;
 -- Second, create objects in the new AM by changing the default AM
 BEGIN;
 SET LOCAL default_table_access_method = 'heap2';
index 9a359466ce49923f9026f3e46d14d28e290aaa22..967bfac21a2efae330940d6f2e486737687111e6 100644 (file)
@@ -161,6 +161,23 @@ WHERE pg_depend.refclassid = 'pg_am'::regclass
     AND pg_am.amname = 'heap2'
 ORDER BY classid, objid, objsubid;
 
+-- ALTER TABLE SET ACCESS METHOD
+CREATE TABLE heaptable USING heap AS
+  SELECT a, repeat(a::text, 100) FROM generate_series(1,9) AS a;
+SELECT amname FROM pg_class c, pg_am am
+  WHERE c.relam = am.oid AND c.oid = 'heaptable'::regclass;
+ALTER TABLE heaptable SET ACCESS METHOD heap2;
+SELECT amname FROM pg_class c, pg_am am
+  WHERE c.relam = am.oid AND c.oid = 'heaptable'::regclass;
+SELECT COUNT(a), COUNT(1) FILTER(WHERE a=1) FROM heaptable;
+-- No support for multiple subcommands
+ALTER TABLE heaptable SET ACCESS METHOD heap, SET ACCESS METHOD heap2;
+DROP TABLE heaptable;
+-- No support for partitioned tables.
+CREATE TABLE am_partitioned(x INT, y INT)
+  PARTITION BY hash (x);
+ALTER TABLE am_partitioned SET ACCESS METHOD heap2;
+DROP TABLE am_partitioned;
 
 -- Second, create objects in the new AM by changing the default AM
 BEGIN;