From 93ab6a72e62fb29e033f4a9d468444269de6b025 Mon Sep 17 00:00:00 2001
From: reshke <reshke@double.cloud>
Date: Sun, 18 Jan 2026 13:33:26 +0000
Subject: [PATCH v1 1/2] bt_page_items pretty-print

---
 contrib/pageinspect/Makefile                  |   2 +-
 contrib/pageinspect/btreefuncs.c              | 137 ++++++++++++++++--
 contrib/pageinspect/meson.build               |   1 +
 .../pageinspect/pageinspect--1.13--1.14.sql   |  17 +++
 contrib/pageinspect/pageinspect.control       |   2 +-
 5 files changed, 147 insertions(+), 12 deletions(-)
 create mode 100644 contrib/pageinspect/pageinspect--1.13--1.14.sql

diff --git a/contrib/pageinspect/Makefile b/contrib/pageinspect/Makefile
index eae989569d0..09774fd340c 100644
--- a/contrib/pageinspect/Makefile
+++ b/contrib/pageinspect/Makefile
@@ -13,7 +13,7 @@ OBJS = \
 	rawpage.o
 
 EXTENSION = pageinspect
-DATA =  pageinspect--1.12--1.13.sql \
+DATA =  pageinspect--1.13--1.14.sql pageinspect--1.12--1.13.sql \
 	pageinspect--1.11--1.12.sql pageinspect--1.10--1.11.sql \
 	pageinspect--1.9--1.10.sql pageinspect--1.8--1.9.sql \
 	pageinspect--1.7--1.8.sql pageinspect--1.6--1.7.sql \
diff --git a/contrib/pageinspect/btreefuncs.c b/contrib/pageinspect/btreefuncs.c
index 0585b7cee40..d8dc59e8c2e 100644
--- a/contrib/pageinspect/btreefuncs.c
+++ b/contrib/pageinspect/btreefuncs.c
@@ -39,6 +39,8 @@
 #include "utils/array.h"
 #include "utils/builtins.h"
 #include "utils/rel.h"
+#include "utils/ruleutils.h"
+#include "utils/lsyscache.h"
 #include "utils/varlena.h"
 
 PG_FUNCTION_INFO_V1(bt_metap);
@@ -96,6 +98,8 @@ typedef struct ua_page_items
 	bool		leafpage;
 	bool		rightmost;
 	TupleDesc	tupd;
+	Relation	indexRel;
+	bool		pretty_print;
 } ua_page_items;
 
 
@@ -547,17 +551,118 @@ bt_page_print_tuples(ua_page_items *uargs)
 	if (dlen < 0 || dlen > INDEX_SIZE_MASK)
 		elog(ERROR, "invalid tuple length %d for tuple at offset number %u",
 			 dlen, offset);
-	dump = palloc0(dlen * 3 + 1);
-	datacstring = dump;
-	for (off = 0; off < dlen; off++)
+
+	if (!uargs->pretty_print)
 	{
-		if (off > 0)
-			*dump++ = ' ';
-		sprintf(dump, "%02x", *(ptr + off) & 0xff);
-		dump += 2;
+		/* Old-style, print hex bytes */
+		dump = palloc0(dlen * 3 + 1);
+		datacstring = dump;
+		for (off = 0; off < dlen; off++)
+		{
+			if (off > 0)
+				*dump++ = ' ';
+			sprintf(dump, "%02x", *(ptr + off) & 0xff);
+			dump += 2;
+		}
+		values[j++] = CStringGetTextDatum(datacstring);
+		pfree(datacstring);
+	}
+	else
+	{
+		/* Do pretty-print, akin to record_out() */
+		StringInfoData buf;
+		TupleDesc tupdesc;
+
+		Datum		itup_values[INDEX_MAX_KEYS];
+		bool		itup_isnull[INDEX_MAX_KEYS];
+		char		*index_columns;
+
+		/*
+		* Included attributes are added when dealing with leaf pages, discarded
+		* for non-leaf pages as these include only data for key attributes.
+		*/
+		int printflags = RULE_INDEXDEF_PRETTY;
+		if (true)
+		{
+			tupdesc = RelationGetDescr(uargs->indexRel);
+		}
+		else
+		{
+			tupdesc = CreateTupleDescTruncatedCopy(RelationGetDescr(uargs->indexRel),
+												IndexRelationGetNumberOfKeyAttributes(uargs->indexRel));
+			printflags |= RULE_INDEXDEF_KEYS_ONLY;
+		}
+
+		index_columns = pg_get_indexdef_columns_extended(RelationGetRelid(uargs->indexRel),
+														printflags);
+
+
+		index_deform_tuple(itup, tupdesc,
+						   itup_values, itup_isnull);
+
+
+		initStringInfo(&buf);
+		appendStringInfo(&buf, "(%s)=(", index_columns);
+
+		for (int i = 0; i < tupdesc->natts; i++)
+		{
+			char	   *value;
+			char	   *tmp;
+			bool		nq = false;
+
+			if (itup_isnull[i])
+				value = "null";
+			else
+			{
+				Oid			foutoid;
+				bool		typisvarlena;
+				Oid			typoid;
+
+				typoid = TupleDescAttr(tupdesc, i)->atttypid;
+				getTypeOutputInfo(typoid, &foutoid, &typisvarlena);
+				value = OidOutputFunctionCall(foutoid, itup_values[i]);
+			}
+
+			if (i == IndexRelationGetNumberOfKeyAttributes(uargs->indexRel))
+				appendStringInfoString(&buf, ") INCLUDE (");
+			else if (i > 0)
+				appendStringInfoString(&buf, ", ");
+
+			/* Check whether we need double quotes for this value */
+			nq = (value[0] == '\0');	/* force quotes for empty string */
+			for (tmp = value; *tmp; tmp++)
+			{
+				char		ch = *tmp;
+
+				if (ch == '"' || ch == '\\' ||
+					ch == '(' || ch == ')' || ch == ',' ||
+					isspace((unsigned char) ch))
+				{
+					nq = true;
+					break;
+				}
+			}
+
+			/* And emit the string */
+			if (nq)
+				appendStringInfoCharMacro(&buf, '"');
+			for (tmp = value; *tmp; tmp++)
+			{
+				char		ch = *tmp;
+
+				if (ch == '"' || ch == '\\')
+					appendStringInfoCharMacro(&buf, ch);
+				appendStringInfoCharMacro(&buf, ch);
+			}
+			if (nq)
+				appendStringInfoCharMacro(&buf, '"');
+		}
+
+		appendStringInfoChar(&buf, ')');
+
+		values[j++] = CStringGetTextDatum(buf.data);
+		pfree(buf.data);
 	}
-	values[j++] = CStringGetTextDatum(datacstring);
-	pfree(datacstring);
 
 	/*
 	 * We need to work around the BTreeTupleIsPivot() !heapkeyspace limitation
@@ -629,6 +734,11 @@ bt_page_items_internal(PG_FUNCTION_ARGS, enum pageinspect_version ext_version)
 	FuncCallContext *fctx;
 	MemoryContext mctx;
 	ua_page_items *uargs;
+	bool		pretty_print = false;
+
+	if (PG_NARGS() >= 3) {
+		pretty_print = PG_GETARG_BOOL(2);
+	}
 
 	if (!superuser())
 		ereport(ERROR,
@@ -666,7 +776,6 @@ bt_page_items_internal(PG_FUNCTION_ARGS, enum pageinspect_version ext_version)
 		memcpy(uargs->page, BufferGetPage(buffer), BLCKSZ);
 
 		UnlockReleaseBuffer(buffer);
-		relation_close(rel, AccessShareLock);
 
 		uargs->offset = FirstOffsetNumber;
 
@@ -683,6 +792,9 @@ bt_page_items_internal(PG_FUNCTION_ARGS, enum pageinspect_version ext_version)
 		uargs->leafpage = P_ISLEAF(opaque);
 		uargs->rightmost = P_RIGHTMOST(opaque);
 
+		uargs->pretty_print = pretty_print;
+		uargs->indexRel = rel;
+
 		/* Build a tuple descriptor for our result type */
 		if (get_call_result_type(fcinfo, NULL, &tupleDesc) != TYPEFUNC_COMPOSITE)
 			elog(ERROR, "return type must be a row type");
@@ -705,6 +817,8 @@ bt_page_items_internal(PG_FUNCTION_ARGS, enum pageinspect_version ext_version)
 		SRF_RETURN_NEXT(fctx, result);
 	}
 
+	relation_close(uargs->indexRel, AccessShareLock);
+
 	SRF_RETURN_DONE(fctx);
 }
 
@@ -798,6 +912,9 @@ bt_page_items_bytea(PG_FUNCTION_ARGS)
 		}
 		uargs->leafpage = P_ISLEAF(opaque);
 		uargs->rightmost = P_RIGHTMOST(opaque);
+		uargs->pretty_print = false;
+		/* For bytea function, we cannot do pretty-print */
+		uargs->indexRel = NULL;
 
 		/* Build a tuple descriptor for our result type */
 		if (get_call_result_type(fcinfo, NULL, &tupleDesc) != TYPEFUNC_COMPOSITE)
diff --git a/contrib/pageinspect/meson.build b/contrib/pageinspect/meson.build
index c43ea400a4d..2f333635838 100644
--- a/contrib/pageinspect/meson.build
+++ b/contrib/pageinspect/meson.build
@@ -38,6 +38,7 @@ install_data(
   'pageinspect--1.10--1.11.sql',
   'pageinspect--1.11--1.12.sql',
   'pageinspect--1.12--1.13.sql',
+  'pageinspect--1.13--1.14.sql',
   'pageinspect.control',
   kwargs: contrib_data_args,
 )
diff --git a/contrib/pageinspect/pageinspect--1.13--1.14.sql b/contrib/pageinspect/pageinspect--1.13--1.14.sql
new file mode 100644
index 00000000000..35d07d7ebe5
--- /dev/null
+++ b/contrib/pageinspect/pageinspect--1.13--1.14.sql
@@ -0,0 +1,17 @@
+
+
+DROP FUNCTION bt_page_items(text, int8);
+CREATE FUNCTION bt_page_items(IN relname text, IN blkno int8,
+    IN pretty_print boolean,
+    OUT itemoffset smallint,
+    OUT ctid tid,
+    OUT itemlen smallint,
+    OUT nulls bool,
+    OUT vars bool,
+    OUT data text,
+    OUT dead boolean,
+    OUT htid tid,
+    OUT tids tid[])
+RETURNS SETOF record
+AS 'MODULE_PATHNAME', 'bt_page_items_1_9'
+LANGUAGE C STRICT PARALLEL SAFE;
diff --git a/contrib/pageinspect/pageinspect.control b/contrib/pageinspect/pageinspect.control
index cfc87feac03..aee3f598a9e 100644
--- a/contrib/pageinspect/pageinspect.control
+++ b/contrib/pageinspect/pageinspect.control
@@ -1,5 +1,5 @@
 # pageinspect extension
 comment = 'inspect the contents of database pages at a low level'
-default_version = '1.13'
+default_version = '1.14'
 module_pathname = '$libdir/pageinspect'
 relocatable = true
-- 
2.43.0

