From: Andrey Rachitskiy <pl0h0yp1@gmail.com>
Date: Fri, 21 Aug 2026 19:05:00 +0500
Subject: [PATCH] Pack short varlenas when flattening expanded objects into tuples

heap_form_tuple already converts ordinary 4-byte-header varlenas to the
1-byte short header when the value is small enough and the attribute is
packable.  The expanded-object path flattened via EOH_flatten_into and
wrote the flattener's mandatory 4-byte-header result directly, so small
arrays (and other expanded types) inserted from PL/pgSQL kept a 4-byte
header and wasted three bytes per value compared with a plain INSERT of
the same datum.

After flattening, apply the same short-header conversion when the flat
size fits.  Flatten into a temporary palloc buffer first because
flatten_into expects a maxaligned destination while short packing does
not.  Do not pfree it, since CurrentMemoryContext may be a bump
allocator.

Author: Andrey Rachitskiy <pl0h0yp1@gmail.com>
Reported-by: anliuan <17801022106@163.com>
Discussion: https://www.postgresql.org/message-id/19636-c44abe40ca7e2c4d@postgresql.org
---
diff --git a/src/backend/access/common/heaptuple.c b/src/backend/access/common/heaptuple.c
index f30346469ed..693d7c751eb 100644
--- a/src/backend/access/common/heaptuple.c
+++ b/src/backend/access/common/heaptuple.c
@@ -87,6 +87,16 @@
 #define COMPACT_ATTR_IS_PACKABLE(att) \
 	((att)->attlen == -1 && (att)->attispackable)
 
+/*
+ * Size rule matching VARATT_CAN_MAKE_SHORT, for use with EOH_get_flat_size()
+ * before flattening.  VARATT_CAN_MAKE_SHORT needs a 4-byte-header (4B_U)
+ * varlena.  An expanded-object toast pointer is not one.
+ */
+#define VARATT_SIZE_CAN_MAKE_SHORT(len) \
+	((len) - VARHDRSZ + VARHDRSZ_SHORT <= VARATT_SHORT_MAX)
+#define VARATT_SHORT_SIZE_FROM_4B(len) \
+	((len) - VARHDRSZ + VARHDRSZ_SHORT)
+
 /*
  * Setup for caching pass-by-ref missing attributes in a way that survives
  * tupleDesc destruction.
@@ -248,11 +258,20 @@ heap_compute_data_size(TupleDesc tupleDesc,
 				 VARATT_IS_EXTERNAL_EXPANDED(DatumGetPointer(val)))
 		{
 			/*
-			 * we want to flatten the expanded value so that the constructed
-			 * tuple doesn't depend on it
+			 * Expanded objects will be flattened into a 4-byte-header
+			 * varlena.  If that fits a short header, account for packing
+			 * (no alignment), matching fill_val.
 			 */
-			data_length = att_nominal_alignby(data_length, atti->attalignby);
-			data_length += EOH_get_flat_size(DatumGetEOHP(val));
+			Size		flat_size = EOH_get_flat_size(DatumGetEOHP(val));
+
+			if (COMPACT_ATTR_IS_PACKABLE(atti) &&
+				VARATT_SIZE_CAN_MAKE_SHORT(flat_size))
+				data_length += VARATT_SHORT_SIZE_FROM_4B(flat_size);
+			else
+			{
+				data_length = att_nominal_alignby(data_length, atti->attalignby);
+				data_length += flat_size;
+			}
 		}
 		else
 		{
@@ -329,14 +348,36 @@ fill_val(CompactAttribute *att,
 			if (VARATT_IS_EXTERNAL_EXPANDED(val))
 			{
 				/*
-				 * we want to flatten the expanded value so that the
-				 * constructed tuple doesn't depend on it
+				 * Flatten so the tuple doesn't depend on the expanded
+				 * object.  Flatteners produce a 4-byte-header varlena.
+				 * Convert to short header when possible.
 				 */
 				ExpandedObjectHeader *eoh = DatumGetEOHP(datum);
-
-				data = (char *) att_nominal_alignby(data, att->attalignby);
-				data_length = EOH_get_flat_size(eoh);
-				EOH_flatten_into(eoh, data, data_length);
+				Size		flat_size = EOH_get_flat_size(eoh);
+
+				if (att->attispackable &&
+					VARATT_SIZE_CAN_MAKE_SHORT(flat_size))
+				{
+					char	   *tmp;
+
+					/*
+					 * Flatten into a temp buffer: EOH_flatten_into needs a
+					 * maxaligned destination, short packing does not.
+					 * Do not pfree(tmp); CurrentMemoryContext may be a bump
+					 * allocator.
+					 */
+					tmp = palloc(flat_size);
+					EOH_flatten_into(eoh, tmp, flat_size);
+					data_length = VARATT_CONVERTED_SHORT_SIZE(tmp);
+					SET_VARSIZE_SHORT(data, data_length);
+					memcpy(data + 1, VARDATA(tmp), data_length - 1);
+				}
+				else
+				{
+					data = (char *) att_nominal_alignby(data, att->attalignby);
+					data_length = flat_size;
+					EOH_flatten_into(eoh, data, data_length);
+				}
 			}
 			else
 			{
diff --git a/src/test/regress/expected/plpgsql.out b/src/test/regress/expected/plpgsql.out
index b37b2abaf80..3def42938d8 100644
--- a/src/test/regress/expected/plpgsql.out
+++ b/src/test/regress/expected/plpgsql.out
@@ -5161,6 +5161,30 @@ begin
   raise notice 'a = %', a;
 end$$;
 NOTICE:  a = {1,2,3}
+-- Inserting an expanded array should use the same short-varlena packing as a
+-- plain INSERT of the equivalent flat value.
+create temp table expanded_short_pack(a text[]);
+insert into expanded_short_pack values ('{aaaaa}');
+create function insert_expanded_short_pack(racl text[]) returns void as $$
+begin
+  insert into expanded_short_pack values (racl);
+end;
+$$ language plpgsql;
+select insert_expanded_short_pack('{aaaaa}');
+ insert_expanded_short_pack 
+----------------------------
+ 
+(1 row)
+
+select pg_column_size(a) from expanded_short_pack;
+ pg_column_size 
+----------------
+             33
+             33
+(2 rows)
+
+drop function insert_expanded_short_pack(text[]);
+drop table expanded_short_pack;
 --
 -- Test access to call stack
 --
diff --git a/src/test/regress/sql/plpgsql.sql b/src/test/regress/sql/plpgsql.sql
index ae6b67e3e22..e97d9a686c5 100644
--- a/src/test/regress/sql/plpgsql.sql
+++ b/src/test/regress/sql/plpgsql.sql
@@ -4160,6 +4160,20 @@ begin
   raise notice 'a = %', a;
 end$$;
 
+-- Inserting an expanded array should use the same short-varlena packing as a
+-- plain INSERT of the equivalent flat value.
+create temp table expanded_short_pack(a text[]);
+insert into expanded_short_pack values ('{aaaaa}');
+create function insert_expanded_short_pack(racl text[]) returns void as $$
+begin
+  insert into expanded_short_pack values (racl);
+end;
+$$ language plpgsql;
+select insert_expanded_short_pack('{aaaaa}');
+select pg_column_size(a) from expanded_short_pack;
+drop function insert_expanded_short_pack(text[]);
+drop table expanded_short_pack;
+
 
 --
 -- Test access to call stack
