]> git.ipfire.org Git - thirdparty/postgresql.git/commitdiff
Take into account default_tablespace during MERGE/SPLIT PARTITION(S)
authorAlexander Korotkov <akorotkov@postgresql.org>
Fri, 26 Jun 2026 12:25:13 +0000 (15:25 +0300)
committerAlexander Korotkov <akorotkov@postgresql.org>
Fri, 26 Jun 2026 12:25:13 +0000 (15:25 +0300)
createPartitionTable() passed the partitioned parent's reltablespace straight
to heap_create_with_catalog(), bypassing the default_tablespace GUC fallback
that DefineRelation() applies for CREATE TABLE ... PARTITION OF.  When the
parent had no explicit tablespace (reltablespace = 0), the new partition
unconditionally landed in the database default, even if default_tablespace
was set to something else; merging or splitting a set of partitions that all
lived in a non-default tablespace produced a new partition in the database
default.

Mirror DefineRelation()'s logic: take parent's reltablespace if set,
otherwise check GetDefaultTablespace() (which reads default_tablespace
and normalises pg_default / MyDatabaseTableSpace to InvalidOid).  Also
add the CREATE ACL check on the resolved tablespace and the pg_global
rejection, matching DefineRelation()'s behavior.

Update the documentation for MERGE/SPLIT PARTITION to spell out the
tablespace-selection rule explicitly.

Reported-by: Justin Pryzby <pryzby@telsasoft.com>
Reviewed-by: Pavel Borisov <pashkin.elfe@gmail.com>
Discussion: https://postgr.es/m/ajQTklv8QArzTp3h%40pryzbyj2023

doc/src/sgml/ref/alter_table.sgml
src/backend/commands/tablecmds.c
src/test/regress/expected/partition_merge.out
src/test/regress/expected/partition_split.out
src/test/regress/sql/partition_merge.sql
src/test/regress/sql/partition_split.sql

index 4ca91f81d3439eb8a457aa3e33d592361f42dbf1..67a05593140c53dbc638db7dfc45e786bafb32a8 100644 (file)
@@ -1234,8 +1234,13 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
      <para>
        <command>ALTER TABLE MERGE PARTITION</command> uses the partitioned
        table itself as the template to construct the new partition.
-       The new partition will inherit the same table access method, persistence
-       type, and tablespace as the partitioned table.
+       The new partition inherits the table access method and persistence type
+       of the partitioned table.  Its tablespace is selected as for a
+       <command>CREATE TABLE ... PARTITION OF</command> command issued
+       without a <literal>TABLESPACE</literal> clause: if the partitioned
+       table has an explicit tablespace, the new partition uses it;
+       otherwise the value of <xref linkend="guc-default-tablespace"/> is
+       taken into account, falling back to the database's default tablespace.
 
        Constraints, column defaults, column generation expressions, identity
        columns, indexes, and triggers are copied from the partitioned table to
@@ -1332,8 +1337,13 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
      <para>
        <command>ALTER TABLE SPLIT PARTITION</command> uses the partitioned
        table itself as the template to construct new partitions.
-       New partitions will inherit the same table access method, persistence
-       type, and tablespace as the partitioned table.
+       New partitions inherit the table access method and persistence type of
+       the partitioned table.  Their tablespace is selected as for a
+       <command>CREATE TABLE ... PARTITION OF</command> command issued
+       without a <literal>TABLESPACE</literal> clause: if the partitioned
+       table has an explicit tablespace, the new partitions use it;
+       otherwise the value of <xref linkend="guc-default-tablespace"/> is
+       taken into account, falling back to the database's default tablespace.
      </para>
 
      <para>
index 265dcfe7fda45df28e5ee76c0506a1e514ca2540..33e065d61ce006b68390b07a9bcbc3c9d5b2eb88 100644 (file)
@@ -22735,6 +22735,7 @@ createPartitionTable(List **wqueue, RangeVar *newPartName,
        Relation        newRel;
        Oid                     newRelId;
        Oid                     existingRelid;
+       Oid                     tablespaceId;
        TupleDesc       descriptor;
        List       *colList = NIL;
        Oid                     relamId;
@@ -22786,10 +22787,38 @@ createPartitionTable(List **wqueue, RangeVar *newPartName,
                                errmsg("cannot create a permanent relation as partition of temporary relation \"%s\"",
                                           RelationGetRelationName(parent_rel)));
 
+       /*
+        * Select the tablespace for the new partition.  Mirror the logic that
+        * CREATE TABLE foo PARTITION OF ... uses in DefineRelation: take the
+        * partitioned parent's explicit tablespace if it has one, otherwise take
+        * default_tablespace into account, and finally use the database default.
+        */
+       tablespaceId = parent_relform->reltablespace;
+       if (!OidIsValid(tablespaceId))
+               tablespaceId = GetDefaultTablespace(newPartName->relpersistence, false);
+
+       /* Check permissions except when using database's default */
+       if (OidIsValid(tablespaceId) && tablespaceId != MyDatabaseTableSpace)
+       {
+               AclResult       aclresult;
+
+               aclresult = object_aclcheck(TableSpaceRelationId, tablespaceId,
+                                                                       GetUserId(), ACL_CREATE);
+               if (aclresult != ACLCHECK_OK)
+                       aclcheck_error(aclresult, OBJECT_TABLESPACE,
+                                                  get_tablespace_name(tablespaceId));
+       }
+
+       /* In all cases disallow placing user relations in pg_global */
+       if (tablespaceId == GLOBALTABLESPACE_OID)
+               ereport(ERROR,
+                               (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+                                errmsg("only shared relations can be placed in pg_global tablespace")));
+
        /* Create the relation. */
        newRelId = heap_create_with_catalog(newPartName->relname,
                                                                                namespaceId,
-                                                                               parent_relform->reltablespace,
+                                                                               tablespaceId,
                                                                                InvalidOid,
                                                                                InvalidOid,
                                                                                InvalidOid,
index 4f42afc3dc7fde5fc0e838c9a3ff5ccc1ae5a006..ccda2b5843b88c7d9b0a5d6260574072a2a2b41c 100644 (file)
@@ -1114,6 +1114,59 @@ SELECT length(a) FROM t;
   10000
 (1 row)
 
+DROP TABLE t;
+-- Tablespace selection for the new merged partition mirrors
+-- CREATE TABLE ... PARTITION OF: the partitioned root's explicit
+-- tablespace wins; otherwise default_tablespace applies; otherwise the
+-- database default is used.
+CREATE TABLE t (i int) PARTITION BY RANGE(i) TABLESPACE regress_tblspace;
+CREATE TABLE tp_0_5 PARTITION OF t FOR VALUES FROM (0) TO (5);
+CREATE TABLE tp_5_10 PARTITION OF t FOR VALUES FROM (5) TO (10);
+INSERT INTO t SELECT generate_series(0, 9);
+ALTER TABLE t MERGE PARTITIONS (tp_0_5, tp_5_10) INTO tp_merged;
+SELECT spcname FROM pg_class c LEFT JOIN pg_tablespace s
+  ON c.reltablespace = s.oid WHERE c.relname = 'tp_merged';
+     spcname      
+------------------
+ regress_tblspace
+(1 row)
+
+DROP TABLE t;
+-- Parent has no explicit tablespace, but default_tablespace is set: the
+-- new partition lands on default_tablespace.
+CREATE TABLE t (i int) PARTITION BY RANGE(i);
+CREATE TABLE tp_0_5 PARTITION OF t FOR VALUES FROM (0) TO (5);
+CREATE TABLE tp_5_10 PARTITION OF t FOR VALUES FROM (5) TO (10);
+INSERT INTO t SELECT generate_series(0, 9);
+SET default_tablespace TO regress_tblspace;
+ALTER TABLE t MERGE PARTITIONS (tp_0_5, tp_5_10) INTO tp_merged;
+RESET default_tablespace;
+SELECT spcname FROM pg_class c LEFT JOIN pg_tablespace s
+  ON c.reltablespace = s.oid WHERE c.relname = 'tp_merged';
+     spcname      
+------------------
+ regress_tblspace
+(1 row)
+
+DROP TABLE t;
+CREATE TABLE t (i int) PARTITION BY RANGE(i);
+CREATE TABLE tp_0_5 PARTITION OF t FOR VALUES FROM (0) TO (5);
+CREATE TABLE tp_5_10 PARTITION OF t FOR VALUES FROM (5) TO (10);
+INSERT INTO t SELECT generate_series(0, 9);
+-- pg_global is rejected when picked up from default_tablespace.
+SET default_tablespace TO pg_global;
+ALTER TABLE t MERGE PARTITIONS (tp_0_5, tp_5_10) INTO tp_merged;       -- fails
+ERROR:  only shared relations can be placed in pg_global tablespace
+RESET default_tablespace;
+-- Parent has no explicit tablespace and default_tablespace is empty: the
+-- new partition uses the database default (reltablespace = 0).
+ALTER TABLE t MERGE PARTITIONS (tp_0_5, tp_5_10) INTO tp_merged;
+SELECT reltablespace FROM pg_class WHERE relname = 'tp_merged';
+ reltablespace 
+---------------
+             0
+(1 row)
+
 DROP TABLE t;
 RESET search_path;
 --
index faaf32ed20a56f7ab83a0baa99e10108a6dbd8e1..8e24556380174be82696f30d4a9eb1d24c7240be 100644 (file)
@@ -1683,6 +1683,74 @@ SELECT length(a) FROM t;
   10000
 (1 row)
 
+DROP TABLE t;
+-- Tablespace selection for the new partitions mirrors
+-- CREATE TABLE ... PARTITION OF: the partitioned root's explicit
+-- tablespace wins; otherwise default_tablespace applies; otherwise the
+-- database default is used.
+CREATE TABLE t (i int) PARTITION BY RANGE(i) TABLESPACE regress_tblspace;
+CREATE TABLE tp_all PARTITION OF t FOR VALUES FROM (0) TO (10);
+INSERT INTO t SELECT generate_series(0, 9);
+ALTER TABLE t SPLIT PARTITION tp_all INTO (
+    PARTITION tp_lo FOR VALUES FROM (0) TO (5),
+    PARTITION tp_hi FOR VALUES FROM (5) TO (10)
+);
+SELECT c.relname, s.spcname FROM pg_class c LEFT JOIN pg_tablespace s
+  ON c.reltablespace = s.oid WHERE c.relname IN ('tp_lo', 'tp_hi')
+  ORDER BY c.relname;
+ relname |     spcname      
+---------+------------------
+ tp_hi   | regress_tblspace
+ tp_lo   | regress_tblspace
+(2 rows)
+
+DROP TABLE t;
+-- Parent has no explicit tablespace, but default_tablespace is set: the
+-- new partitions land on default_tablespace.
+CREATE TABLE t (i int) PARTITION BY RANGE(i);
+CREATE TABLE tp_all PARTITION OF t FOR VALUES FROM (0) TO (10);
+INSERT INTO t SELECT generate_series(0, 9);
+SET default_tablespace TO regress_tblspace;
+ALTER TABLE t SPLIT PARTITION tp_all INTO (
+    PARTITION tp_lo FOR VALUES FROM (0) TO (5),
+    PARTITION tp_hi FOR VALUES FROM (5) TO (10)
+);
+RESET default_tablespace;
+SELECT c.relname, s.spcname FROM pg_class c LEFT JOIN pg_tablespace s
+  ON c.reltablespace = s.oid WHERE c.relname IN ('tp_lo', 'tp_hi')
+  ORDER BY c.relname;
+ relname |     spcname      
+---------+------------------
+ tp_hi   | regress_tblspace
+ tp_lo   | regress_tblspace
+(2 rows)
+
+DROP TABLE t;
+CREATE TABLE t (i int) PARTITION BY RANGE(i);
+CREATE TABLE tp_all PARTITION OF t FOR VALUES FROM (0) TO (10);
+INSERT INTO t SELECT generate_series(0, 9);
+-- pg_global is rejected when picked up from default_tablespace.
+SET default_tablespace TO pg_global;
+ALTER TABLE t SPLIT PARTITION tp_all INTO (
+    PARTITION tp_lo FOR VALUES FROM (0) TO (5),
+    PARTITION tp_hi FOR VALUES FROM (5) TO (10)
+);     -- fails
+ERROR:  only shared relations can be placed in pg_global tablespace
+RESET default_tablespace;
+-- Parent has no explicit tablespace and default_tablespace is empty: new
+-- partitions use the database default (reltablespace = 0).
+ALTER TABLE t SPLIT PARTITION tp_all INTO (
+    PARTITION tp_lo FOR VALUES FROM (0) TO (5),
+    PARTITION tp_hi FOR VALUES FROM (5) TO (10)
+);
+SELECT relname, reltablespace FROM pg_class
+  WHERE relname IN ('tp_lo', 'tp_hi') ORDER BY relname;
+ relname | reltablespace 
+---------+---------------
+ tp_hi   |             0
+ tp_lo   |             0
+(2 rows)
+
 DROP TABLE t;
 RESET search_path;
 --
index 4c8c625f97b6a74cff5869abbb4f71fbc08f3e52..80dc365b0cef05dd488cbd3132ae0b39ca831af6 100644 (file)
@@ -798,6 +798,46 @@ SELECT reltoastrelid <> 0 AS has_toast,
 SELECT length(a) FROM t;
 DROP TABLE t;
 
+-- Tablespace selection for the new merged partition mirrors
+-- CREATE TABLE ... PARTITION OF: the partitioned root's explicit
+-- tablespace wins; otherwise default_tablespace applies; otherwise the
+-- database default is used.
+CREATE TABLE t (i int) PARTITION BY RANGE(i) TABLESPACE regress_tblspace;
+CREATE TABLE tp_0_5 PARTITION OF t FOR VALUES FROM (0) TO (5);
+CREATE TABLE tp_5_10 PARTITION OF t FOR VALUES FROM (5) TO (10);
+INSERT INTO t SELECT generate_series(0, 9);
+ALTER TABLE t MERGE PARTITIONS (tp_0_5, tp_5_10) INTO tp_merged;
+SELECT spcname FROM pg_class c LEFT JOIN pg_tablespace s
+  ON c.reltablespace = s.oid WHERE c.relname = 'tp_merged';
+DROP TABLE t;
+
+-- Parent has no explicit tablespace, but default_tablespace is set: the
+-- new partition lands on default_tablespace.
+CREATE TABLE t (i int) PARTITION BY RANGE(i);
+CREATE TABLE tp_0_5 PARTITION OF t FOR VALUES FROM (0) TO (5);
+CREATE TABLE tp_5_10 PARTITION OF t FOR VALUES FROM (5) TO (10);
+INSERT INTO t SELECT generate_series(0, 9);
+SET default_tablespace TO regress_tblspace;
+ALTER TABLE t MERGE PARTITIONS (tp_0_5, tp_5_10) INTO tp_merged;
+RESET default_tablespace;
+SELECT spcname FROM pg_class c LEFT JOIN pg_tablespace s
+  ON c.reltablespace = s.oid WHERE c.relname = 'tp_merged';
+DROP TABLE t;
+
+CREATE TABLE t (i int) PARTITION BY RANGE(i);
+CREATE TABLE tp_0_5 PARTITION OF t FOR VALUES FROM (0) TO (5);
+CREATE TABLE tp_5_10 PARTITION OF t FOR VALUES FROM (5) TO (10);
+INSERT INTO t SELECT generate_series(0, 9);
+-- pg_global is rejected when picked up from default_tablespace.
+SET default_tablespace TO pg_global;
+ALTER TABLE t MERGE PARTITIONS (tp_0_5, tp_5_10) INTO tp_merged;       -- fails
+RESET default_tablespace;
+-- Parent has no explicit tablespace and default_tablespace is empty: the
+-- new partition uses the database default (reltablespace = 0).
+ALTER TABLE t MERGE PARTITIONS (tp_0_5, tp_5_10) INTO tp_merged;
+SELECT reltablespace FROM pg_class WHERE relname = 'tp_merged';
+DROP TABLE t;
+
 
 RESET search_path;
 
index 9e44aa9caf045612f71453ca99fc2ce454c8d787..ffd15e7f969013198c2103a1d58ddba3c91d42d3 100644 (file)
@@ -1204,6 +1204,58 @@ SELECT relname,
 SELECT length(a) FROM t;
 DROP TABLE t;
 
+-- Tablespace selection for the new partitions mirrors
+-- CREATE TABLE ... PARTITION OF: the partitioned root's explicit
+-- tablespace wins; otherwise default_tablespace applies; otherwise the
+-- database default is used.
+CREATE TABLE t (i int) PARTITION BY RANGE(i) TABLESPACE regress_tblspace;
+CREATE TABLE tp_all PARTITION OF t FOR VALUES FROM (0) TO (10);
+INSERT INTO t SELECT generate_series(0, 9);
+ALTER TABLE t SPLIT PARTITION tp_all INTO (
+    PARTITION tp_lo FOR VALUES FROM (0) TO (5),
+    PARTITION tp_hi FOR VALUES FROM (5) TO (10)
+);
+SELECT c.relname, s.spcname FROM pg_class c LEFT JOIN pg_tablespace s
+  ON c.reltablespace = s.oid WHERE c.relname IN ('tp_lo', 'tp_hi')
+  ORDER BY c.relname;
+DROP TABLE t;
+
+-- Parent has no explicit tablespace, but default_tablespace is set: the
+-- new partitions land on default_tablespace.
+CREATE TABLE t (i int) PARTITION BY RANGE(i);
+CREATE TABLE tp_all PARTITION OF t FOR VALUES FROM (0) TO (10);
+INSERT INTO t SELECT generate_series(0, 9);
+SET default_tablespace TO regress_tblspace;
+ALTER TABLE t SPLIT PARTITION tp_all INTO (
+    PARTITION tp_lo FOR VALUES FROM (0) TO (5),
+    PARTITION tp_hi FOR VALUES FROM (5) TO (10)
+);
+RESET default_tablespace;
+SELECT c.relname, s.spcname FROM pg_class c LEFT JOIN pg_tablespace s
+  ON c.reltablespace = s.oid WHERE c.relname IN ('tp_lo', 'tp_hi')
+  ORDER BY c.relname;
+DROP TABLE t;
+
+CREATE TABLE t (i int) PARTITION BY RANGE(i);
+CREATE TABLE tp_all PARTITION OF t FOR VALUES FROM (0) TO (10);
+INSERT INTO t SELECT generate_series(0, 9);
+-- pg_global is rejected when picked up from default_tablespace.
+SET default_tablespace TO pg_global;
+ALTER TABLE t SPLIT PARTITION tp_all INTO (
+    PARTITION tp_lo FOR VALUES FROM (0) TO (5),
+    PARTITION tp_hi FOR VALUES FROM (5) TO (10)
+);     -- fails
+RESET default_tablespace;
+-- Parent has no explicit tablespace and default_tablespace is empty: new
+-- partitions use the database default (reltablespace = 0).
+ALTER TABLE t SPLIT PARTITION tp_all INTO (
+    PARTITION tp_lo FOR VALUES FROM (0) TO (5),
+    PARTITION tp_hi FOR VALUES FROM (5) TO (10)
+);
+SELECT relname, reltablespace FROM pg_class
+  WHERE relname IN ('tp_lo', 'tp_hi') ORDER BY relname;
+DROP TABLE t;
+
 RESET search_path;
 
 --