diff --git a/doc/src/sgml/func/allfiles.sgml b/doc/src/sgml/func/allfiles.sgml
index ce11ef1d5d8..f5e3f008537 100644
--- a/doc/src/sgml/func/allfiles.sgml
+++ b/doc/src/sgml/func/allfiles.sgml
@@ -17,6 +17,7 @@ Complete list of usable sgml source files in this directory.
+
diff --git a/doc/src/sgml/func/func-tid.sgml b/doc/src/sgml/func/func-tid.sgml
new file mode 100644
index 00000000000..188e66a181f
--- /dev/null
+++ b/doc/src/sgml/func/func-tid.sgml
@@ -0,0 +1,70 @@
+
+ TID Functions
+
+
+ TID
+ functions
+
+
+
+ tid_block
+
+
+
+ tid_offset
+
+
+
+ lists functions for
+ the tid data type (tuple identifier).
+
+
+
+ TID Functions
+
+
+
+
+ Function
+
+
+ Description
+
+
+ Example(s)
+
+
+
+
+
+
+
+ tid_block ( tid )
+ bigint
+
+
+ Extracts the block number from a tuple identifier.
+
+
+ tid_block('(42,7)'::tid)
+ 42
+
+
+
+
+
+ tid_offset ( tid )
+ integer
+
+
+ Extracts the tuple offset within the block from a tuple identifier.
+
+
+ tid_offset('(42,7)'::tid)
+ 7
+
+
+
+
+
+
diff --git a/doc/src/sgml/func/func.sgml b/doc/src/sgml/func/func.sgml
index f351ef53f63..c9c231dd190 100644
--- a/doc/src/sgml/func/func.sgml
+++ b/doc/src/sgml/func/func.sgml
@@ -62,6 +62,7 @@ repeat('Pg', 4) PgPgPgPg
&func-geometry;
&func-net;
&func-textsearch;
+&func-tid;
&func-uuid;
&func-xml;
&func-json;
diff --git a/src/backend/utils/adt/tid.c b/src/backend/utils/adt/tid.c
index 07248b69e57..9257886f1da 100644
--- a/src/backend/utils/adt/tid.c
+++ b/src/backend/utils/adt/tid.c
@@ -279,6 +279,35 @@ hashtidextended(PG_FUNCTION_ARGS)
seed);
}
+/*
+ * Extract the block number from a TID
+ *
+ * Returns int8 because BlockNumber is uint32, which exceeds the range of int4.
+ */
+Datum
+tid_block(PG_FUNCTION_ARGS)
+{
+ ItemPointer tid = PG_GETARG_ITEMPOINTER(0);
+
+ /* need to use NoCheck, as tidin allows InvalidBlockNumber */
+ PG_RETURN_INT64((int64) ItemPointerGetBlockNumberNoCheck(tid));
+}
+
+/*
+ * Extract the offset number from a TID
+ *
+ * Returns int4 because OffsetNumber is uint16, which exceeds the range of
+ * int2.
+ */
+Datum
+tid_offset(PG_FUNCTION_ARGS)
+{
+ ItemPointer tid = PG_GETARG_ITEMPOINTER(0);
+
+ /* need to use NoCheck, as tidin allows InvalidOffsetNumber */
+ PG_RETURN_INT32((int32) ItemPointerGetOffsetNumberNoCheck(tid));
+}
+
/*
* Functions to get latest tid of a specified tuple.
diff --git a/src/include/catalog/catversion.h b/src/include/catalog/catversion.h
index 582bb2e2058..0cee5315b59 100644
--- a/src/include/catalog/catversion.h
+++ b/src/include/catalog/catversion.h
@@ -57,6 +57,6 @@
*/
/* yyyymmddN */
-#define CATALOG_VERSION_NO 202604031
+#define CATALOG_VERSION_NO 202604051
#endif
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index cbf85d6a5be..3ea17fc5629 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -2757,6 +2757,12 @@
{ oid => '2796', descr => 'smaller of two',
proname => 'tidsmaller', prorettype => 'tid', proargtypes => 'tid tid',
prosrc => 'tidsmaller' },
+{ oid => '9951', descr => 'extract block number from tid',
+ proname => 'tid_block', proleakproof => 't', prorettype => 'int8',
+ proargtypes => 'tid', prosrc => 'tid_block' },
+{ oid => '9952', descr => 'extract offset number from tid',
+ proname => 'tid_offset', proleakproof => 't', prorettype => 'int4',
+ proargtypes => 'tid', prosrc => 'tid_offset' },
{ oid => '2233', descr => 'hash',
proname => 'hashtid', prorettype => 'int4', proargtypes => 'tid',
prosrc => 'hashtid' },
diff --git a/src/test/regress/expected/opr_sanity.out b/src/test/regress/expected/opr_sanity.out
index 6ff4d7ee901..cfdc6b1a17a 100644
--- a/src/test/regress/expected/opr_sanity.out
+++ b/src/test/regress/expected/opr_sanity.out
@@ -887,6 +887,8 @@ oid8le(oid8,oid8)
oid8gt(oid8,oid8)
oid8ge(oid8,oid8)
btoid8cmp(oid8,oid8)
+tid_block(tid)
+tid_offset(tid)
-- Check that functions without argument are not marked as leakproof.
SELECT p1.oid::regprocedure
FROM pg_proc p1 JOIN pg_namespace pn
diff --git a/src/test/regress/expected/tid.out b/src/test/regress/expected/tid.out
index 083c83a1e1b..3497a77688b 100644
--- a/src/test/regress/expected/tid.out
+++ b/src/test/regress/expected/tid.out
@@ -42,6 +42,57 @@ SELECT * FROM pg_input_error_info('(0,-1)', 'tid');
invalid input syntax for type tid: "(0,-1)" | | | 22P02
(1 row)
+-- tests for tid_block() and tid_offset()
+SELECT tid_block('(0,0)'::tid), tid_offset('(0,0)'::tid);
+ tid_block | tid_offset
+-----------+------------
+ 0 | 0
+(1 row)
+
+SELECT tid_block('(0,1)'::tid), tid_offset('(0,1)'::tid);
+ tid_block | tid_offset
+-----------+------------
+ 0 | 1
+(1 row)
+
+SELECT tid_block('(42,7)'::tid), tid_offset('(42,7)'::tid);
+ tid_block | tid_offset
+-----------+------------
+ 42 | 7
+(1 row)
+
+-- max values: blockno uint32 max, offset uint16 max
+SELECT tid_block('(4294967295,65535)'::tid), tid_offset('(4294967295,65535)'::tid);
+ tid_block | tid_offset
+------------+------------
+ 4294967295 | 65535
+(1 row)
+
+-- (-1,0) wraps to blockno 4294967295
+SELECT tid_block('(-1,0)'::tid);
+ tid_block
+------------
+ 4294967295
+(1 row)
+
+-- NULL handling (strict functions)
+SELECT tid_block(NULL::tid), tid_offset(NULL::tid);
+ tid_block | tid_offset
+-----------+------------
+ |
+(1 row)
+
+-- round-trip: blockno + offset reconstruct the original TID
+SELECT t, tid_block(t), tid_offset(t),
+ format('(%s,%s)', tid_block(t), tid_offset(t))::tid = t AS roundtrip_ok
+FROM (VALUES ('(0,0)'::tid), ('(1,42)'::tid), ('(4294967295,65535)'::tid)) AS v(t);
+ t | tid_block | tid_offset | roundtrip_ok
+--------------------+------------+------------+--------------
+ (0,0) | 0 | 0 | t
+ (1,42) | 1 | 42 | t
+ (4294967295,65535) | 4294967295 | 65535 | t
+(3 rows)
+
-- tests for functions related to TID handling
CREATE TABLE tid_tab (a int);
-- min() and max() for TIDs
@@ -58,6 +109,21 @@ SELECT max(ctid) FROM tid_tab;
(0,2)
(1 row)
+-- tid_block() and tid_offset() with real table ctid
+SELECT ctid, tid_block(ctid), tid_offset(ctid) FROM tid_tab;
+ ctid | tid_block | tid_offset
+-------+-----------+------------
+ (0,1) | 0 | 1
+ (0,2) | 0 | 2
+(2 rows)
+
+-- use in WHERE clause
+SELECT ctid FROM tid_tab WHERE tid_block(ctid) = 0 AND tid_offset(ctid) = 1;
+ ctid
+-------
+ (0,1)
+(1 row)
+
TRUNCATE tid_tab;
-- Tests for currtid2() with various relation kinds
-- Materialized view
diff --git a/src/test/regress/sql/tid.sql b/src/test/regress/sql/tid.sql
index 2602e20eb5a..c0a70be5cbd 100644
--- a/src/test/regress/sql/tid.sql
+++ b/src/test/regress/sql/tid.sql
@@ -16,6 +16,22 @@ SELECT pg_input_is_valid('(0,-1)', 'tid');
SELECT * FROM pg_input_error_info('(0,-1)', 'tid');
+-- tests for tid_block() and tid_offset()
+SELECT tid_block('(0,0)'::tid), tid_offset('(0,0)'::tid);
+SELECT tid_block('(0,1)'::tid), tid_offset('(0,1)'::tid);
+SELECT tid_block('(42,7)'::tid), tid_offset('(42,7)'::tid);
+-- max values: blockno uint32 max, offset uint16 max
+SELECT tid_block('(4294967295,65535)'::tid), tid_offset('(4294967295,65535)'::tid);
+-- (-1,0) wraps to blockno 4294967295
+SELECT tid_block('(-1,0)'::tid);
+-- NULL handling (strict functions)
+SELECT tid_block(NULL::tid), tid_offset(NULL::tid);
+-- round-trip: blockno + offset reconstruct the original TID
+SELECT t, tid_block(t), tid_offset(t),
+ format('(%s,%s)', tid_block(t), tid_offset(t))::tid = t AS roundtrip_ok
+FROM (VALUES ('(0,0)'::tid), ('(1,42)'::tid), ('(4294967295,65535)'::tid)) AS v(t);
+
+
-- tests for functions related to TID handling
CREATE TABLE tid_tab (a int);
@@ -24,6 +40,11 @@ CREATE TABLE tid_tab (a int);
INSERT INTO tid_tab VALUES (1), (2);
SELECT min(ctid) FROM tid_tab;
SELECT max(ctid) FROM tid_tab;
+
+-- tid_block() and tid_offset() with real table ctid
+SELECT ctid, tid_block(ctid), tid_offset(ctid) FROM tid_tab;
+-- use in WHERE clause
+SELECT ctid FROM tid_tab WHERE tid_block(ctid) = 0 AND tid_offset(ctid) = 1;
TRUNCATE tid_tab;
-- Tests for currtid2() with various relation kinds