v1-0001-Create-TOAST-table-for-partitions-made-by-MERGE-S.patch

application/octet-stream

Filename: v1-0001-Create-TOAST-table-for-partitions-made-by-MERGE-S.patch
Type: application/octet-stream
Part: 0
Message: Re: Add SPLIT PARTITION/MERGE PARTITIONS commands

Patch

Same data as JSON: GET /api/v1/attachments/:id/patch the parsed metadata as JSON — format, series position, per-file stats; never the diff bytes. API reference →
Format: format-patch
Series: patch v1-0001
Subject: Create TOAST table for partitions made by MERGE/SPLIT PARTITION
File+
src/backend/commands/tablecmds.c 9 0
src/test/regress/expected/partition_merge.out 23 0
src/test/regress/expected/partition_split.out 28 0
src/test/regress/sql/partition_merge.sql 14 0
src/test/regress/sql/partition_split.sql 17 0
From 9ee64c74480a5bb61062aa3f173f7ec98b2ce8ad Mon Sep 17 00:00:00 2001
From: Alexander Korotkov <akorotkov@postgresql.org>
Date: Mon, 15 Jun 2026 15:05:23 +0300
Subject: [PATCH v1] Create TOAST table for partitions made by MERGE/SPLIT
 PARTITION

ALTER TABLE ... MERGE PARTITIONS / SPLIT PARTITION builds a new
partition via createPartitionTable(), but never gave it a TOAST table.
When the source rows carried out-of-line varlena values, the move
into the new partition entered heap_toast_insert_or_update() with
reltoastrelid = InvalidOid: the externalization step is skipped, the
value falls back to inline storage and heap_insert() fails with
"row is too big" error.  Also, TOAST table is needed if the new partition
receives out-of-line varlena values after the DDL operation is complete.

Call NewRelationCreateToastTable() right after the new partition is
created in createPartitionTable(), mirroring what DefineRelation()
does for regular CREATE TABLE.  NewRelationCreateToastTable() decides
on its own whether a TOAST table is actually required, so partitions
with no toast-eligible columns are unaffected.

Reported-by: Justin Pryzby <pryzby@telsasoft.com>
Discussion: https://postgr.es/m/ai_c4-v8iLA2kXFV%40pryzbyj2023
---
 src/backend/commands/tablecmds.c              |  9 ++++++
 src/test/regress/expected/partition_merge.out | 23 +++++++++++++++
 src/test/regress/expected/partition_split.out | 28 +++++++++++++++++++
 src/test/regress/sql/partition_merge.sql      | 14 ++++++++++
 src/test/regress/sql/partition_split.sql      | 17 +++++++++++
 5 files changed, 91 insertions(+)

diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 38f9ffcd04f..265dcfe7fda 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -22815,6 +22815,15 @@ createPartitionTable(List **wqueue, RangeVar *newPartName,
 	 */
 	CommandCounterIncrement();
 
+	/*
+	 * Create a TOAST table if the table needs one.  MERGE/SPLIT PARTITION
+	 * moves rows from existing partition(s) into new partition(s), which may
+	 * carry out-of-line varlena values that the new relation must be able to
+	 * store.  Also, the new partition must be able to receive out-of-line
+	 * varlena values after the DDL operation is complete.
+	 */
+	NewRelationCreateToastTable(newRelId, (Datum) 0);
+
 	/*
 	 * Open the new partition with no lock, because we already have an
 	 * AccessExclusiveLock placed there after creation.
diff --git a/src/test/regress/expected/partition_merge.out b/src/test/regress/expected/partition_merge.out
index d3818f1bf9b..49ffa9a0631 100644
--- a/src/test/regress/expected/partition_merge.out
+++ b/src/test/regress/expected/partition_merge.out
@@ -1088,6 +1088,29 @@ SELECT count(*) FROM t WHERE i = 15 AND g IN (SELECT g + 10 FROM t WHERE i = 5);
      1
 (1 row)
 
+DROP TABLE t;
+-- Merged partitions need their own TOAST table; otherwise an out-of-line
+-- varlena values can't be stored.
+CREATE TABLE t (a text) PARTITION BY RANGE(a);
+CREATE TABLE tp_def PARTITION OF t DEFAULT;
+CREATE TABLE tp_2_3 PARTITION OF t FOR VALUES FROM ('2') TO ('3');
+-- Long value: out-of-line storage even after pglz compression
+INSERT INTO t SELECT repeat('1', 9999999);
+ALTER TABLE t MERGE PARTITIONS (tp_def, tp_2_3) INTO tp_merged;
+SELECT reltoastrelid <> 0 AS has_toast,
+       pg_relation_size(reltoastrelid) > 0 AS toast_used
+  FROM pg_class WHERE relname = 'tp_merged';
+ has_toast | toast_used 
+-----------+------------
+ t         | t
+(1 row)
+
+SELECT length(a) FROM t;
+ length  
+---------
+ 9999999
+(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 ff6027af658..d651767c8b8 100644
--- a/src/test/regress/expected/partition_split.out
+++ b/src/test/regress/expected/partition_split.out
@@ -1653,6 +1653,34 @@ SELECT count(*) FROM t WHERE i = 0 AND tab_id IN (SELECT tab_id FROM t WHERE i =
      0
 (1 row)
 
+DROP TABLE t;
+-- Each new partition produced by SPLIT must get its own TOAST table so
+-- that out-of-line varlena attributes coming from the source partition
+-- can be stored.
+CREATE TABLE t (a text) PARTITION BY RANGE(a);
+CREATE TABLE tp_all PARTITION OF t FOR VALUES FROM (MINVALUE) TO (MAXVALUE);
+-- Long value: out-of-line storage even after pglz compression
+INSERT INTO t SELECT repeat('1', 9999999);
+ALTER TABLE t SPLIT PARTITION tp_all INTO (
+    PARTITION tp_lo FOR VALUES FROM (MINVALUE) TO ('2'),
+    PARTITION tp_hi FOR VALUES FROM ('2') TO (MAXVALUE)
+);
+SELECT relname,
+       reltoastrelid <> 0 AS has_toast,
+       pg_relation_size(reltoastrelid) > 0 AS toast_used
+  FROM pg_class WHERE relname IN ('tp_lo', 'tp_hi') ORDER BY relname;
+ relname | has_toast | toast_used 
+---------+-----------+------------
+ tp_hi   | t         | f
+ tp_lo   | t         | t
+(2 rows)
+
+SELECT length(a) FROM t;
+ length  
+---------
+ 9999999
+(1 row)
+
 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 1e14ed40f5c..7a9d43552a7 100644
--- a/src/test/regress/sql/partition_merge.sql
+++ b/src/test/regress/sql/partition_merge.sql
@@ -781,6 +781,20 @@ SELECT count(*) FROM t WHERE i = 15 AND g IN (SELECT g + 10 FROM t WHERE i = 5);
 
 DROP TABLE t;
 
+-- Merged partitions need their own TOAST table; otherwise an out-of-line
+-- varlena values can't be stored.
+CREATE TABLE t (a text) PARTITION BY RANGE(a);
+CREATE TABLE tp_def PARTITION OF t DEFAULT;
+CREATE TABLE tp_2_3 PARTITION OF t FOR VALUES FROM ('2') TO ('3');
+-- Long value: out-of-line storage even after pglz compression
+INSERT INTO t SELECT repeat('1', 9999999);
+ALTER TABLE t MERGE PARTITIONS (tp_def, tp_2_3) INTO tp_merged;
+SELECT reltoastrelid <> 0 AS has_toast,
+       pg_relation_size(reltoastrelid) > 0 AS toast_used
+  FROM pg_class WHERE relname = 'tp_merged';
+SELECT length(a) FROM t;
+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 05de24152d1..06636a366a5 100644
--- a/src/test/regress/sql/partition_split.sql
+++ b/src/test/regress/sql/partition_split.sql
@@ -1184,6 +1184,23 @@ SELECT count(*) FROM t WHERE i = 0 AND tab_id IN (SELECT tab_id FROM t WHERE i =
 
 DROP TABLE t;
 
+-- Each new partition produced by SPLIT must get its own TOAST table so
+-- that out-of-line varlena attributes coming from the source partition
+-- can be stored.
+CREATE TABLE t (a text) PARTITION BY RANGE(a);
+CREATE TABLE tp_all PARTITION OF t FOR VALUES FROM (MINVALUE) TO (MAXVALUE);
+-- Long value: out-of-line storage even after pglz compression
+INSERT INTO t SELECT repeat('1', 9999999);
+ALTER TABLE t SPLIT PARTITION tp_all INTO (
+    PARTITION tp_lo FOR VALUES FROM (MINVALUE) TO ('2'),
+    PARTITION tp_hi FOR VALUES FROM ('2') TO (MAXVALUE)
+);
+SELECT relname,
+       reltoastrelid <> 0 AS has_toast,
+       pg_relation_size(reltoastrelid) > 0 AS toast_used
+  FROM pg_class WHERE relname IN ('tp_lo', 'tp_hi') ORDER BY relname;
+SELECT length(a) FROM t;
+DROP TABLE t;
 
 RESET search_path;
 
-- 
2.39.5 (Apple Git-154)