From f995f7bc64ceea94dba4b576b4aad2faa8a0834f Mon Sep 17 00:00:00 2001 From: Alexander Korotkov Date: Thu, 18 Jun 2026 21:53:32 +0300 Subject: [PATCH v2] 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 Reviewed-by: Pavel Borisov Discussion: https://postgr.es/m/ajQTklv8QArzTp3h%40pryzbyj2023 --- doc/src/sgml/ref/alter_table.sgml | 18 +++-- src/backend/commands/tablecmds.c | 31 ++++++++- src/test/regress/expected/partition_merge.out | 54 +++++++++++++++ src/test/regress/expected/partition_split.out | 69 +++++++++++++++++++ src/test/regress/sql/partition_merge.sql | 41 +++++++++++ src/test/regress/sql/partition_split.sql | 53 ++++++++++++++ 6 files changed, 261 insertions(+), 5 deletions(-) diff --git a/doc/src/sgml/ref/alter_table.sgml b/doc/src/sgml/ref/alter_table.sgml index dec34337d1a..36c840f68d8 100644 --- a/doc/src/sgml/ref/alter_table.sgml +++ b/doc/src/sgml/ref/alter_table.sgml @@ -1233,8 +1233,13 @@ WITH ( MODULUS numeric_literal, REM ALTER TABLE MERGE PARTITION 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 + CREATE TABLE ... PARTITION OF command issued + without a TABLESPACE clause: if the partitioned + table has an explicit tablespace, the new partition uses it; + otherwise the value of 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 @@ -1331,8 +1336,13 @@ WITH ( MODULUS numeric_literal, REM ALTER TABLE SPLIT PARTITION 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 + CREATE TABLE ... PARTITION OF command issued + without a TABLESPACE clause: if the partitioned + table has an explicit tablespace, the new partitions use it; + otherwise the value of is + taken into account, falling back to the database's default tablespace. diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 265dcfe7fda..33e065d61ce 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -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, diff --git a/src/test/regress/expected/partition_merge.out b/src/test/regress/expected/partition_merge.out index 4f42afc3dc7..d0e1804c24f 100644 --- a/src/test/regress/expected/partition_merge.out +++ b/src/test/regress/expected/partition_merge.out @@ -1114,6 +1114,60 @@ 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; +-- Parent has no explicit tablespace and default_tablespace is empty: the +-- new partition uses the database default (reltablespace = 0). Also +-- exercise the pg_global rejection path with default_tablespace pointing +-- at the shared 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 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; +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; -- diff --git a/src/test/regress/expected/partition_split.out b/src/test/regress/expected/partition_split.out index faaf32ed20a..58241a03d3f 100644 --- a/src/test/regress/expected/partition_split.out +++ b/src/test/regress/expected/partition_split.out @@ -1683,6 +1683,75 @@ 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; +-- Parent has no explicit tablespace and default_tablespace is empty: new +-- partitions use the database default (reltablespace = 0). Also exercise +-- the pg_global rejection path with default_tablespace pointing at the +-- shared 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 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; +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; -- diff --git a/src/test/regress/sql/partition_merge.sql b/src/test/regress/sql/partition_merge.sql index 4c8c625f97b..d857b4d2fb6 100644 --- a/src/test/regress/sql/partition_merge.sql +++ b/src/test/regress/sql/partition_merge.sql @@ -798,6 +798,47 @@ 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; + +-- Parent has no explicit tablespace and default_tablespace is empty: the +-- new partition uses the database default (reltablespace = 0). Also +-- exercise the pg_global rejection path with default_tablespace pointing +-- at the shared 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 pg_global; +ALTER TABLE t MERGE PARTITIONS (tp_0_5, tp_5_10) INTO tp_merged; -- fails +RESET default_tablespace; +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; diff --git a/src/test/regress/sql/partition_split.sql b/src/test/regress/sql/partition_split.sql index 9e44aa9caf0..e73811670cd 100644 --- a/src/test/regress/sql/partition_split.sql +++ b/src/test/regress/sql/partition_split.sql @@ -1204,6 +1204,59 @@ 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; + +-- Parent has no explicit tablespace and default_tablespace is empty: new +-- partitions use the database default (reltablespace = 0). Also exercise +-- the pg_global rejection path with default_tablespace pointing at the +-- shared 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 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; +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; -- -- 2.39.5 (Apple Git-154)