public inbox for [email protected]  
help / color / mirror / Atom feed
From: Ayush Tiwari <[email protected]>
To: [email protected]
Subject: tid_blockno() and tid_offset() accessor functions
Date: Sat, 28 Feb 2026 00:29:20 +0530
Message-ID: <CAJTYsWUzok2+mvSYkbVUwq_SWWg-GdHqCuYumN82AU97SjwjCA@mail.gmail.com> (raw)

Hi hackers,

As of now we don't have any built-in way to extract the block and offset
components from a TID. When people need to group by page (like for bloat
analysis) or filter by specific blocks, they usually end up using the
`ctid::text::point` hack:

    SELECT (ctid::text::point)[0]::bigint AS blockno,
           (ctid::text::point)[1]::int    AS offset
    FROM my_table;

This works, but it's pretty clunky, relies on the text representation, and
isn't great if you're trying to parse TIDs outside of SQL.

The attached patch adds two simple accessor functions:
- `tid_blockno(tid) -> bigint`
- `tid_offset(tid) -> integer`

A couple of quick notes on the implementation I went for:
- `tid_blockno` returns `int8` since `BlockNumber` is `uint32` and could
overflow `int4`.
- `tid_offset` returns `int4` since `OffsetNumber` is `uint16`.
- Both are marked leakproof and strict.
- I used the `NoCheck` macros from `itemptr.h` so they safely handle
user-supplied literals like `(0,0)`.

Please let me know what you think!

Regards,
Ayush


Attachments:

  [application/octet-stream] 0001-Add-tid_blockno-and-tid_offset-accessor-functions.patch (7.8K, 3-0001-Add-tid_blockno-and-tid_offset-accessor-functions.patch)
  download | inline diff:
From 02b652640e3eebeb575de1a1f36fb5013cdf0585 Mon Sep 17 00:00:00 2001
From: Ayush Tiwari <[email protected]>
Date: Fri, 27 Feb 2026 18:48:11 +0000
Subject: [PATCH v1] Add tid_blockno() and tid_offset() accessor functions

Add two new built-in SQL functions to extract the components of a tid
(tuple identifier) value:

  tid_blockno(tid) -> bigint   -- extract block number
  tid_offset(tid)  -> integer  -- extract offset number

These provide a clean, efficient alternative to the common workaround
of ctid::text::point for decomposing TID values. The text-based hack
is fragile, inefficient, and unavailable outside of SQL contexts.

tid_blockno() returns int8 (bigint) because BlockNumber is uint32,
which exceeds the range of int4. tid_offset() returns int4 (integer)
because OffsetNumber is uint16, which fits safely in int4.

Both functions use the NoCheck accessor variants from itemptr.h,
are marked leakproof, and include regression tests covering typical
values, boundary conditions, NULL handling, and round-trip identity.
---
 src/backend/utils/adt/tid.c              | 31 +++++++++++
 src/include/catalog/pg_proc.dat          |  6 +++
 src/test/regress/expected/opr_sanity.out |  2 +
 src/test/regress/expected/tid.out        | 66 ++++++++++++++++++++++++
 src/test/regress/sql/tid.sql             | 21 ++++++++
 5 files changed, 126 insertions(+)

diff --git a/src/backend/utils/adt/tid.c b/src/backend/utils/adt/tid.c
index 07248b69e57..fbf29afac10 100644
--- a/src/backend/utils/adt/tid.c
+++ b/src/backend/utils/adt/tid.c
@@ -280,6 +280,37 @@ hashtidextended(PG_FUNCTION_ARGS)
 }
 
 
+/*
+ *	TID accessor functions
+ */
+
+/*
+ * tid_blockno - extract the block number from a TID
+ *
+ * Returns int8 because BlockNumber is uint32, which exceeds the range of int4.
+ */
+Datum
+tid_blockno(PG_FUNCTION_ARGS)
+{
+	ItemPointer tid = PG_GETARG_ITEMPOINTER(0);
+
+	PG_RETURN_INT64((int64) ItemPointerGetBlockNumberNoCheck(tid));
+}
+
+/*
+ * tid_offset - extract the offset number from a TID
+ *
+ * Returns int4 because OffsetNumber is uint16, which fits in int4.
+ */
+Datum
+tid_offset(PG_FUNCTION_ARGS)
+{
+	ItemPointer tid = PG_GETARG_ITEMPOINTER(0);
+
+	PG_RETURN_INT32((int32) ItemPointerGetOffsetNumberNoCheck(tid));
+}
+
+
 /*
  *	Functions to get latest tid of a specified tuple.
  *
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index dac40992cbc..fc0fdc6cd1b 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -2747,6 +2747,12 @@
 { oid => '2796', descr => 'smaller of two',
   proname => 'tidsmaller', prorettype => 'tid', proargtypes => 'tid tid',
   prosrc => 'tidsmaller' },
+{ oid => '9949', descr => 'extract block number from tid',
+  proname => 'tid_blockno', proleakproof => 't', prorettype => 'int8',
+  proargtypes => 'tid', prosrc => 'tid_blockno' },
+{ oid => '9950', 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..005d99058f6 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_blockno(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..4e21014c81f 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_blockno() and tid_offset()
+SELECT tid_blockno('(0,0)'::tid), tid_offset('(0,0)'::tid);
+ tid_blockno | tid_offset 
+-------------+------------
+           0 |          0
+(1 row)
+
+SELECT tid_blockno('(0,1)'::tid), tid_offset('(0,1)'::tid);
+ tid_blockno | tid_offset 
+-------------+------------
+           0 |          1
+(1 row)
+
+SELECT tid_blockno('(42,7)'::tid), tid_offset('(42,7)'::tid);
+ tid_blockno | tid_offset 
+-------------+------------
+          42 |          7
+(1 row)
+
+-- max values: blockno uint32 max, offset uint16 max
+SELECT tid_blockno('(4294967295,65535)'::tid), tid_offset('(4294967295,65535)'::tid);
+ tid_blockno | tid_offset 
+-------------+------------
+  4294967295 |      65535
+(1 row)
+
+-- (-1,0) wraps to blockno 4294967295
+SELECT tid_blockno('(-1,0)'::tid);
+ tid_blockno 
+-------------
+  4294967295
+(1 row)
+
+-- NULL handling (strict functions)
+SELECT tid_blockno(NULL::tid), tid_offset(NULL::tid);
+ tid_blockno | tid_offset 
+-------------+------------
+             |           
+(1 row)
+
+-- round-trip: blockno + offset reconstruct the original TID
+SELECT t, tid_blockno(t), tid_offset(t),
+       format('(%s,%s)', tid_blockno(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_blockno | 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_blockno() and tid_offset() with real table ctid
+SELECT ctid, tid_blockno(ctid), tid_offset(ctid) FROM tid_tab;
+ ctid  | tid_blockno | tid_offset 
+-------+-------------+------------
+ (0,1) |           0 |          1
+ (0,2) |           0 |          2
+(2 rows)
+
+-- use in WHERE clause
+SELECT ctid FROM tid_tab WHERE tid_blockno(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..75370833cd0 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_blockno() and tid_offset()
+SELECT tid_blockno('(0,0)'::tid), tid_offset('(0,0)'::tid);
+SELECT tid_blockno('(0,1)'::tid), tid_offset('(0,1)'::tid);
+SELECT tid_blockno('(42,7)'::tid), tid_offset('(42,7)'::tid);
+-- max values: blockno uint32 max, offset uint16 max
+SELECT tid_blockno('(4294967295,65535)'::tid), tid_offset('(4294967295,65535)'::tid);
+-- (-1,0) wraps to blockno 4294967295
+SELECT tid_blockno('(-1,0)'::tid);
+-- NULL handling (strict functions)
+SELECT tid_blockno(NULL::tid), tid_offset(NULL::tid);
+-- round-trip: blockno + offset reconstruct the original TID
+SELECT t, tid_blockno(t), tid_offset(t),
+       format('(%s,%s)', tid_blockno(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_blockno() and tid_offset() with real table ctid
+SELECT ctid, tid_blockno(ctid), tid_offset(ctid) FROM tid_tab;
+-- use in WHERE clause
+SELECT ctid FROM tid_tab WHERE tid_blockno(ctid) = 0 AND tid_offset(ctid) = 1;
 TRUNCATE tid_tab;
 
 -- Tests for currtid2() with various relation kinds
-- 
2.45.4



view thread (18+ messages)  latest in thread

reply

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Reply to all the recipients using the --to and --cc options:
  reply via email

  To: [email protected]
  Cc: [email protected]
  Subject: Re: tid_blockno() and tid_offset() accessor functions
  In-Reply-To: <CAJTYsWUzok2+mvSYkbVUwq_SWWg-GdHqCuYumN82AU97SjwjCA@mail.gmail.com>

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

This inbox is served by agora; see mirroring instructions
for how to clone and mirror all data and code used for this inbox