Take into account default_tablespace during MERGE/SPLIT PARTITION(S)

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
This commit is contained in:
Alexander Korotkov 2026-06-26 15:25:13 +03:00
parent 30937c60cd
commit cdae794af3
6 changed files with 257 additions and 5 deletions

View 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>

View 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,

View 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;
--

View 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;
--

View 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;

View 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;
--