From 4538e2ea3d687f1cac5ddbccf4364b73c093e8da Mon Sep 17 00:00:00 2001 From: "Chao Li (Evan)" Date: Thu, 1 Oct 2026 06:52:59 +0800 Subject: [PATCH v2 1/2] pg_walinspect: add function to list WAL files by LSN Add pg_get_wal_files(), which returns the retained WAL segment files intersecting an LSN range, together with each segment's complete LSN boundaries. A point lookup can be requested by omitting end_lsn or making it equal to start_lsn. Choose segment file names from the server's current timeline history so that ranges spanning a timeline switch are handled correctly. Author: Chao Li Reviewed-by: Surya Poondla Discussion: https://postgr.es/m/80E9F0AD-CFC5-4BE5-81DE-D8FE35E10A1C@gmail.com --- contrib/pg_walinspect/Makefile | 4 +- .../pg_walinspect/expected/oldextversions.out | 8 +- .../pg_walinspect/expected/pg_walinspect.out | 73 ++++++ contrib/pg_walinspect/meson.build | 6 + .../pg_walinspect/pg_walinspect--1.1--1.2.sql | 22 ++ contrib/pg_walinspect/pg_walinspect.c | 209 +++++++++++++++++- contrib/pg_walinspect/pg_walinspect.control | 2 +- contrib/pg_walinspect/sql/oldextversions.sql | 5 +- contrib/pg_walinspect/sql/pg_walinspect.sql | 34 +++ contrib/pg_walinspect/t/001_timeline.pl | 62 ++++++ doc/src/sgml/pgwalinspect.sgml | 34 ++- src/tools/pgindent/typedefs.list | 1 + 12 files changed, 441 insertions(+), 19 deletions(-) create mode 100644 contrib/pg_walinspect/pg_walinspect--1.1--1.2.sql create mode 100644 contrib/pg_walinspect/t/001_timeline.pl diff --git a/contrib/pg_walinspect/Makefile b/contrib/pg_walinspect/Makefile index 22090f7716c..b7651231077 100644 --- a/contrib/pg_walinspect/Makefile +++ b/contrib/pg_walinspect/Makefile @@ -7,9 +7,11 @@ OBJS = \ PGFILEDESC = "pg_walinspect - functions to inspect contents of PostgreSQL Write-Ahead Log" EXTENSION = pg_walinspect -DATA = pg_walinspect--1.0.sql pg_walinspect--1.0--1.1.sql +DATA = pg_walinspect--1.0.sql pg_walinspect--1.0--1.1.sql \ + pg_walinspect--1.1--1.2.sql REGRESS = pg_walinspect oldextversions +TAP_TESTS = 1 REGRESS_OPTS = --temp-config $(top_srcdir)/contrib/pg_walinspect/walinspect.conf diff --git a/contrib/pg_walinspect/expected/oldextversions.out b/contrib/pg_walinspect/expected/oldextversions.out index 89953af3d0e..311017d66ca 100644 --- a/contrib/pg_walinspect/expected/oldextversions.out +++ b/contrib/pg_walinspect/expected/oldextversions.out @@ -46,18 +46,20 @@ SELECT * FROM pg_get_wal_records_info_till_end_of_wal('FFFFFFFF/FFFFFFFF'); ERROR: WAL start LSN must be less than current LSN SELECT * FROM pg_get_wal_stats_till_end_of_wal('FFFFFFFF/FFFFFFFF'); ERROR: WAL start LSN must be less than current LSN --- Move to new version 1.1. +-- Test each upgrade step through the current version. ALTER EXTENSION pg_walinspect UPDATE TO '1.1'; --- List what version 1.1 contains. +ALTER EXTENSION pg_walinspect UPDATE TO '1.2'; +-- List what the current version contains. \dx+ pg_walinspect Objects in extension "pg_walinspect" Object description ------------------------------------------------------- function pg_get_wal_block_info(pg_lsn,pg_lsn,boolean) + function pg_get_wal_files(pg_lsn,pg_lsn) function pg_get_wal_record_info(pg_lsn) function pg_get_wal_records_info(pg_lsn,pg_lsn) function pg_get_wal_stats(pg_lsn,pg_lsn,boolean) -(4 rows) +(5 rows) SELECT pg_drop_replication_slot('regress_pg_walinspect_slot'); pg_drop_replication_slot diff --git a/contrib/pg_walinspect/expected/pg_walinspect.out b/contrib/pg_walinspect/expected/pg_walinspect.out index f955ff5d3c5..34864c65ec7 100644 --- a/contrib/pg_walinspect/expected/pg_walinspect.out +++ b/contrib/pg_walinspect/expected/pg_walinspect.out @@ -90,6 +90,54 @@ SELECT COUNT(*) >= 1 AS ok FROM pg_get_wal_block_info(:'wal_lsn1', :'wal_lsn2'); t (1 row) +-- Return the retained WAL file and its complete segment boundaries. +SELECT wal_file = pg_walfile_name(segment_start_lsn) AS file_ok, + segment_start_lsn <= :'wal_lsn1'::pg_lsn AS start_ok, + segment_end_lsn > :'wal_lsn1'::pg_lsn AS end_ok +FROM pg_get_wal_files(:'wal_lsn1', :'wal_lsn1'::pg_lsn + 1); + file_ok | start_ok | end_ok +---------+----------+-------- + t | t | t +(1 row) + +-- An end LSN at a segment boundary must not include the next segment. +WITH segment AS +( + SELECT * FROM pg_get_wal_files(:'wal_lsn1', :'wal_lsn1'::pg_lsn + 1) +) +SELECT count(*) = 1 AS one_file +FROM segment, + LATERAL pg_get_wal_files(segment_start_lsn, segment_end_lsn); + one_file +---------- + t +(1 row) + +SELECT count(*) = 1 AS equal_lsn_ok +FROM pg_get_wal_files(:'wal_lsn1', :'wal_lsn1'); + equal_lsn_ok +-------------- + t +(1 row) + +SELECT count(*) = 1 AS default_end_ok +FROM pg_get_wal_files(:'wal_lsn1'); + default_end_ok +---------------- + t +(1 row) + +SELECT * FROM pg_get_wal_files(:'wal_lsn1'::pg_lsn + 1, :'wal_lsn1'); +ERROR: WAL start LSN must be less than end LSN +SELECT count(*) = 1 AS future_end_ok +FROM pg_get_wal_files(pg_current_wal_lsn(), 'FFFFFFFF/FFFFFFFF'); + future_end_ok +--------------- + t +(1 row) + +SELECT * FROM pg_get_wal_files('0/0', '0/1'); +ERROR: could not read WAL at LSN 0/00000000 -- =================================================================== -- Test for filtering out WAL records of a particular table -- =================================================================== @@ -183,6 +231,13 @@ SELECT has_function_privilege('regress_pg_walinspect', f (1 row) +SELECT has_function_privilege('regress_pg_walinspect', + 'pg_get_wal_files(pg_lsn, pg_lsn)', 'EXECUTE'); -- no + has_function_privilege +------------------------ + f +(1 row) + -- Functions accessible by users with role pg_read_server_files. GRANT pg_read_server_files TO regress_pg_walinspect; SELECT has_function_privilege('regress_pg_walinspect', @@ -213,6 +268,13 @@ SELECT has_function_privilege('regress_pg_walinspect', t (1 row) +SELECT has_function_privilege('regress_pg_walinspect', + 'pg_get_wal_files(pg_lsn, pg_lsn)', 'EXECUTE'); -- yes + has_function_privilege +------------------------ + t +(1 row) + REVOKE pg_read_server_files FROM regress_pg_walinspect; -- Superuser can grant execute to other users. GRANT EXECUTE ON FUNCTION pg_get_wal_record_info(pg_lsn) @@ -223,6 +285,8 @@ GRANT EXECUTE ON FUNCTION pg_get_wal_stats(pg_lsn, pg_lsn, boolean) TO regress_pg_walinspect; GRANT EXECUTE ON FUNCTION pg_get_wal_block_info(pg_lsn, pg_lsn, boolean) TO regress_pg_walinspect; +GRANT EXECUTE ON FUNCTION pg_get_wal_files(pg_lsn, pg_lsn) + TO regress_pg_walinspect; SELECT has_function_privilege('regress_pg_walinspect', 'pg_get_wal_record_info(pg_lsn)', 'EXECUTE'); -- yes has_function_privilege @@ -251,6 +315,13 @@ SELECT has_function_privilege('regress_pg_walinspect', t (1 row) +SELECT has_function_privilege('regress_pg_walinspect', + 'pg_get_wal_files(pg_lsn, pg_lsn)', 'EXECUTE'); -- yes + has_function_privilege +------------------------ + t +(1 row) + REVOKE EXECUTE ON FUNCTION pg_get_wal_record_info(pg_lsn) FROM regress_pg_walinspect; REVOKE EXECUTE ON FUNCTION pg_get_wal_records_info(pg_lsn, pg_lsn) @@ -259,6 +330,8 @@ REVOKE EXECUTE ON FUNCTION pg_get_wal_stats(pg_lsn, pg_lsn, boolean) FROM regress_pg_walinspect; REVOKE EXECUTE ON FUNCTION pg_get_wal_block_info(pg_lsn, pg_lsn, boolean) FROM regress_pg_walinspect; +REVOKE EXECUTE ON FUNCTION pg_get_wal_files(pg_lsn, pg_lsn) + FROM regress_pg_walinspect; -- =================================================================== -- Clean up -- =================================================================== diff --git a/contrib/pg_walinspect/meson.build b/contrib/pg_walinspect/meson.build index 82ab186c340..6f8b4a6359a 100644 --- a/contrib/pg_walinspect/meson.build +++ b/contrib/pg_walinspect/meson.build @@ -20,6 +20,7 @@ install_data( 'pg_walinspect.control', 'pg_walinspect--1.0.sql', 'pg_walinspect--1.0--1.1.sql', + 'pg_walinspect--1.1--1.2.sql', kwargs: contrib_data_args, ) @@ -37,4 +38,9 @@ tests += { 'regress_args': ['--temp-config', files('walinspect.conf')], 'runningcheck': false, }, + 'tap': { + 'tests': [ + 't/001_timeline.pl', + ], + }, } diff --git a/contrib/pg_walinspect/pg_walinspect--1.1--1.2.sql b/contrib/pg_walinspect/pg_walinspect--1.1--1.2.sql new file mode 100644 index 00000000000..9cc9523b4dc --- /dev/null +++ b/contrib/pg_walinspect/pg_walinspect--1.1--1.2.sql @@ -0,0 +1,22 @@ +/* contrib/pg_walinspect/pg_walinspect--1.1--1.2.sql */ + +-- complain if script is sourced in psql, rather than via ALTER EXTENSION +\echo Use "ALTER EXTENSION pg_walinspect UPDATE TO '1.2'" to load this file. \quit + +-- +-- pg_get_wal_files() +-- +CREATE FUNCTION pg_get_wal_files( + IN start_lsn pg_lsn, + IN end_lsn pg_lsn DEFAULT NULL, + OUT wal_file text, + OUT segment_start_lsn pg_lsn, + OUT segment_end_lsn pg_lsn +) +RETURNS SETOF record +AS 'MODULE_PATHNAME', 'pg_get_wal_files' +LANGUAGE C PARALLEL SAFE; + +REVOKE EXECUTE ON FUNCTION pg_get_wal_files(pg_lsn, pg_lsn) FROM PUBLIC; +GRANT EXECUTE ON FUNCTION pg_get_wal_files(pg_lsn, pg_lsn) + TO pg_read_server_files; diff --git a/contrib/pg_walinspect/pg_walinspect.c b/contrib/pg_walinspect/pg_walinspect.c index a172f9e2b40..42f366bdb4e 100644 --- a/contrib/pg_walinspect/pg_walinspect.c +++ b/contrib/pg_walinspect/pg_walinspect.c @@ -13,6 +13,7 @@ #include "postgres.h" #include "access/htup_details.h" +#include "access/timeline.h" #include "access/xlog.h" #include "access/xlog_internal.h" #include "access/xlogreader.h" @@ -22,6 +23,7 @@ #include "funcapi.h" #include "miscadmin.h" #include "port/pg_bitutils.h" +#include "storage/fd.h" #include "utils/array.h" #include "utils/builtins.h" #include "utils/pg_lsn.h" @@ -38,6 +40,7 @@ PG_MODULE_MAGIC_EXT( ); PG_FUNCTION_INFO_V1(pg_get_wal_block_info); +PG_FUNCTION_INFO_V1(pg_get_wal_files); PG_FUNCTION_INFO_V1(pg_get_wal_record_info); PG_FUNCTION_INFO_V1(pg_get_wal_records_info); PG_FUNCTION_INFO_V1(pg_get_wal_records_info_till_end_of_wal); @@ -45,7 +48,7 @@ PG_FUNCTION_INFO_V1(pg_get_wal_stats); PG_FUNCTION_INFO_V1(pg_get_wal_stats_till_end_of_wal); static void ValidateInputLSNs(XLogRecPtr start_lsn, XLogRecPtr *end_lsn); -static XLogRecPtr GetCurrentLSN(void); +static XLogRecPtr GetCurrentLSN(TimeLineID *current_tli); static XLogReaderState *InitXLogReaderState(XLogRecPtr lsn); static XLogRecord *ReadNextXLogRecord(XLogReaderState *xlogreader); static void GetWALRecordInfo(XLogReaderState *record, Datum *values, @@ -68,11 +71,21 @@ static void GetWalStats(FunctionCallInfo fcinfo, static void GetWALBlockInfo(FunctionCallInfo fcinfo, XLogReaderState *record, bool show_data); +/* Metadata for one retained segment in the current timeline's history. */ +typedef struct WalTimeSegment +{ + XLogSegNo segno; /* segment number, used for WAL ordering */ + TimeLineID tli; /* timeline containing this segment */ +} WalTimeSegment; + +static int wal_time_segment_cmp(const void *a, const void *b); +static WalTimeSegment *GetWalTimeSegments(int *nsegments); + /* - * Return the LSN up to which the server has WAL. + * Return the LSN up to which the server has WAL, and optionally its timeline. */ static XLogRecPtr -GetCurrentLSN(void) +GetCurrentLSN(TimeLineID *current_tli) { XLogRecPtr curr_lsn; @@ -81,9 +94,15 @@ GetCurrentLSN(void) * callback read_local_xlog_page_no_wait does. */ if (!RecoveryInProgress()) - curr_lsn = GetFlushRecPtr(NULL); + curr_lsn = GetFlushRecPtr(current_tli); else - curr_lsn = GetXLogReplayRecPtr(NULL); + { + curr_lsn = GetXLogReplayRecPtr(current_tli); + if (current_tli != NULL && *current_tli == 0) + { + *current_tli = GetWALInsertionTimeLineIfSet(); + } + } Assert(XLogRecPtrIsValid(curr_lsn)); @@ -190,6 +209,94 @@ ReadNextXLogRecord(XLogReaderState *xlogreader) return record; } +/* qsort comparator that puts segments in WAL order. */ +static int +wal_time_segment_cmp(const void *a, const void *b) +{ + const WalTimeSegment *seg1 = (const WalTimeSegment *) a; + const WalTimeSegment *seg2 = (const WalTimeSegment *) b; + + if (seg1->segno < seg2->segno) + return -1; + if (seg1->segno > seg2->segno) + return 1; + return 0; +} + +/* + * Get the WAL segments that form the history of the server's current + * timeline. Other timelines may have files with the same segment number in + * pg_wal, so choose the timeline that is valid at the end of each segment, + * as read_local_xlog_page_no_wait() does. Return the segments in WAL order. + */ +static WalTimeSegment * +GetWalTimeSegments(int *nsegments) +{ + WalTimeSegment *segments = NULL; + DIR *dir; + struct dirent *de; + List *history; + XLogRecPtr current_lsn; + TimeLineID current_tli; + int allocated = 0; + int count = 0; + + current_lsn = GetCurrentLSN(¤t_tli); + + history = readTimeLineHistory(current_tli); + dir = AllocateDir(XLOGDIR); + while ((de = ReadDir(dir, XLOGDIR)) != NULL) + { + TimeLineID file_tli; + XLogSegNo segno; + XLogRecPtr seg_start; + XLogRecPtr seg_end; + + if (!IsXLogFileName(de->d_name)) + continue; + + XLogFromFileName(de->d_name, &file_tli, &segno, wal_segment_size); + seg_start = segno * wal_segment_size; + seg_end = seg_start + wal_segment_size - 1; + + if (seg_start >= current_lsn || + file_tli != tliOfPointInHistory(seg_end, history)) + continue; + + if (count == allocated) + { + allocated = allocated ? allocated * 2 : 16; + if (segments == NULL) + segments = palloc_array(WalTimeSegment, allocated); + else + segments = repalloc_array(segments, WalTimeSegment, allocated); + } + + MemSet(&segments[count], 0, sizeof(WalTimeSegment)); + segments[count].segno = segno; + segments[count].tli = file_tli; + count++; + } + FreeDir(dir); + list_free_deep(history); + + if (count == 0) + ereport(ERROR, + errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), + errmsg("no retained WAL segments are available")); + + qsort(segments, count, sizeof(WalTimeSegment), wal_time_segment_cmp); + +#ifdef USE_ASSERT_CHECKING + /* A segment number identifies at most one file in this timeline history. */ + for (int i = 1; i < count; i++) + Assert(segments[i - 1].segno != segments[i].segno); +#endif + + *nsegments = count; + return segments; +} + /* * Output values that make up a row describing caller's WAL record. * @@ -483,7 +590,7 @@ pg_get_wal_record_info(PG_FUNCTION_ARGS) HeapTuple tuple; lsn = PG_GETARG_LSN(0); - curr_lsn = GetCurrentLSN(); + curr_lsn = GetCurrentLSN(NULL); if (lsn > curr_lsn) ereport(ERROR, @@ -516,6 +623,90 @@ pg_get_wal_record_info(PG_FUNCTION_ARGS) #undef PG_GET_WAL_RECORD_INFO_COLS } +/* + * List the retained WAL segment files intersecting [start_lsn, end_lsn), or + * the segment containing start_lsn when end_lsn is omitted or equal to it. + * Report complete segment boundaries, not the range clipped to the inputs. + */ +Datum +pg_get_wal_files(PG_FUNCTION_ARGS) +{ +#define PG_GET_WAL_FILES_COLS 3 + XLogRecPtr start_lsn; + XLogRecPtr end_lsn; + XLogSegNo first_segno; + XLogSegNo last_segno; + bool point_lookup; + WalTimeSegment *segments; + ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo; + int nsegments; + int segment_index = 0; + + InitMaterializedSRF(fcinfo, 0); + + /* A NULL start produces an empty set. */ + if (PG_ARGISNULL(0)) + PG_RETURN_VOID(); + + start_lsn = PG_GETARG_LSN(0); + end_lsn = PG_ARGISNULL(1) ? start_lsn : PG_GETARG_LSN(1); + + if (start_lsn < XLOG_BLCKSZ) + ereport(ERROR, + errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("could not read WAL at LSN %X/%08X", + LSN_FORMAT_ARGS(start_lsn))); + + ValidateInputLSNs(start_lsn, &end_lsn); + point_lookup = start_lsn == end_lsn; + + + XLByteToSeg(start_lsn, first_segno, wal_segment_size); + if (point_lookup) + last_segno = first_segno; + else + XLByteToPrevSeg(end_lsn, last_segno, wal_segment_size); + segments = GetWalTimeSegments(&nsegments); + + for (XLogSegNo segno = first_segno;; segno++) + { + Datum values[PG_GET_WAL_FILES_COLS]; + bool nulls[PG_GET_WAL_FILES_COLS] = {0}; + XLogRecPtr segment_start_lsn = segno * wal_segment_size; + XLogRecPtr segment_end_lsn = segment_start_lsn + wal_segment_size; + char fname[MAXFNAMELEN]; + + while (segment_index < nsegments && + segments[segment_index].segno < segno) + segment_index++; + + if (segment_index >= nsegments || + segments[segment_index].segno != segno) + ereport(ERROR, + errcode(ERRCODE_DATA_EXCEPTION), + errmsg("WAL segment needed for the requested range is missing"), + errdetail("Segment " UINT64_FORMAT " is not present in pg_wal.", + (uint64) segno)); + + XLogFileName(fname, segments[segment_index].tli, segno, + wal_segment_size); + values[0] = CStringGetTextDatum(fname); + values[1] = LSNGetDatum(segment_start_lsn); + values[2] = LSNGetDatum(segment_end_lsn); + tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc, + values, nulls); + + if (segno == last_segno) + break; + + CHECK_FOR_INTERRUPTS(); + } + + pfree(segments); + PG_RETURN_VOID(); +#undef PG_GET_WAL_FILES_COLS +} + /* * Validate start and end LSNs coming from the function inputs. * @@ -525,7 +716,7 @@ pg_get_wal_record_info(PG_FUNCTION_ARGS) static void ValidateInputLSNs(XLogRecPtr start_lsn, XLogRecPtr *end_lsn) { - XLogRecPtr curr_lsn = GetCurrentLSN(); + XLogRecPtr curr_lsn = GetCurrentLSN(NULL); if (start_lsn > curr_lsn) ereport(ERROR, @@ -831,7 +1022,7 @@ Datum pg_get_wal_records_info_till_end_of_wal(PG_FUNCTION_ARGS) { XLogRecPtr start_lsn = PG_GETARG_LSN(0); - XLogRecPtr end_lsn = GetCurrentLSN(); + XLogRecPtr end_lsn = GetCurrentLSN(NULL); if (start_lsn > end_lsn) ereport(ERROR, @@ -849,7 +1040,7 @@ Datum pg_get_wal_stats_till_end_of_wal(PG_FUNCTION_ARGS) { XLogRecPtr start_lsn = PG_GETARG_LSN(0); - XLogRecPtr end_lsn = GetCurrentLSN(); + XLogRecPtr end_lsn = GetCurrentLSN(NULL); bool stats_per_record = PG_GETARG_BOOL(1); if (start_lsn > end_lsn) diff --git a/contrib/pg_walinspect/pg_walinspect.control b/contrib/pg_walinspect/pg_walinspect.control index efa3cb2cfe5..5f574b865bb 100644 --- a/contrib/pg_walinspect/pg_walinspect.control +++ b/contrib/pg_walinspect/pg_walinspect.control @@ -1,5 +1,5 @@ # pg_walinspect extension comment = 'functions to inspect contents of PostgreSQL Write-Ahead Log' -default_version = '1.1' +default_version = '1.2' module_pathname = '$libdir/pg_walinspect' relocatable = true diff --git a/contrib/pg_walinspect/sql/oldextversions.sql b/contrib/pg_walinspect/sql/oldextversions.sql index e35c4f3ed15..b935a067b3e 100644 --- a/contrib/pg_walinspect/sql/oldextversions.sql +++ b/contrib/pg_walinspect/sql/oldextversions.sql @@ -27,10 +27,11 @@ SELECT COUNT(*) >= 1 AS ok FROM pg_get_wal_stats_till_end_of_wal(:'wal_lsn1'); SELECT * FROM pg_get_wal_records_info_till_end_of_wal('FFFFFFFF/FFFFFFFF'); SELECT * FROM pg_get_wal_stats_till_end_of_wal('FFFFFFFF/FFFFFFFF'); --- Move to new version 1.1. +-- Test each upgrade step through the current version. ALTER EXTENSION pg_walinspect UPDATE TO '1.1'; +ALTER EXTENSION pg_walinspect UPDATE TO '1.2'; --- List what version 1.1 contains. +-- List what the current version contains. \dx+ pg_walinspect SELECT pg_drop_replication_slot('regress_pg_walinspect_slot'); diff --git a/contrib/pg_walinspect/sql/pg_walinspect.sql b/contrib/pg_walinspect/sql/pg_walinspect.sql index 1e64a22d29a..3176a17d855 100644 --- a/contrib/pg_walinspect/sql/pg_walinspect.sql +++ b/contrib/pg_walinspect/sql/pg_walinspect.sql @@ -51,6 +51,30 @@ SELECT COUNT(*) >= 1 AS ok FROM pg_get_wal_records_info(:'wal_lsn1', :'wal_lsn2' SELECT COUNT(*) >= 1 AS ok FROM pg_get_wal_stats(:'wal_lsn1', :'wal_lsn2'); SELECT COUNT(*) >= 1 AS ok FROM pg_get_wal_block_info(:'wal_lsn1', :'wal_lsn2'); +-- Return the retained WAL file and its complete segment boundaries. +SELECT wal_file = pg_walfile_name(segment_start_lsn) AS file_ok, + segment_start_lsn <= :'wal_lsn1'::pg_lsn AS start_ok, + segment_end_lsn > :'wal_lsn1'::pg_lsn AS end_ok +FROM pg_get_wal_files(:'wal_lsn1', :'wal_lsn1'::pg_lsn + 1); + +-- An end LSN at a segment boundary must not include the next segment. +WITH segment AS +( + SELECT * FROM pg_get_wal_files(:'wal_lsn1', :'wal_lsn1'::pg_lsn + 1) +) +SELECT count(*) = 1 AS one_file +FROM segment, + LATERAL pg_get_wal_files(segment_start_lsn, segment_end_lsn); + +SELECT count(*) = 1 AS equal_lsn_ok +FROM pg_get_wal_files(:'wal_lsn1', :'wal_lsn1'); +SELECT count(*) = 1 AS default_end_ok +FROM pg_get_wal_files(:'wal_lsn1'); +SELECT * FROM pg_get_wal_files(:'wal_lsn1'::pg_lsn + 1, :'wal_lsn1'); +SELECT count(*) = 1 AS future_end_ok +FROM pg_get_wal_files(pg_current_wal_lsn(), 'FFFFFFFF/FFFFFFFF'); +SELECT * FROM pg_get_wal_files('0/0', '0/1'); + -- =================================================================== -- Test for filtering out WAL records of a particular table -- =================================================================== @@ -110,6 +134,8 @@ SELECT has_function_privilege('regress_pg_walinspect', 'pg_get_wal_stats(pg_lsn, pg_lsn, boolean) ', 'EXECUTE'); -- no SELECT has_function_privilege('regress_pg_walinspect', 'pg_get_wal_block_info(pg_lsn, pg_lsn, boolean) ', 'EXECUTE'); -- no +SELECT has_function_privilege('regress_pg_walinspect', + 'pg_get_wal_files(pg_lsn, pg_lsn)', 'EXECUTE'); -- no -- Functions accessible by users with role pg_read_server_files. GRANT pg_read_server_files TO regress_pg_walinspect; @@ -122,6 +148,8 @@ SELECT has_function_privilege('regress_pg_walinspect', 'pg_get_wal_stats(pg_lsn, pg_lsn, boolean) ', 'EXECUTE'); -- yes SELECT has_function_privilege('regress_pg_walinspect', 'pg_get_wal_block_info(pg_lsn, pg_lsn, boolean) ', 'EXECUTE'); -- yes +SELECT has_function_privilege('regress_pg_walinspect', + 'pg_get_wal_files(pg_lsn, pg_lsn)', 'EXECUTE'); -- yes REVOKE pg_read_server_files FROM regress_pg_walinspect; @@ -134,6 +162,8 @@ GRANT EXECUTE ON FUNCTION pg_get_wal_stats(pg_lsn, pg_lsn, boolean) TO regress_pg_walinspect; GRANT EXECUTE ON FUNCTION pg_get_wal_block_info(pg_lsn, pg_lsn, boolean) TO regress_pg_walinspect; +GRANT EXECUTE ON FUNCTION pg_get_wal_files(pg_lsn, pg_lsn) + TO regress_pg_walinspect; SELECT has_function_privilege('regress_pg_walinspect', 'pg_get_wal_record_info(pg_lsn)', 'EXECUTE'); -- yes @@ -143,6 +173,8 @@ SELECT has_function_privilege('regress_pg_walinspect', 'pg_get_wal_stats(pg_lsn, pg_lsn, boolean) ', 'EXECUTE'); -- yes SELECT has_function_privilege('regress_pg_walinspect', 'pg_get_wal_block_info(pg_lsn, pg_lsn, boolean) ', 'EXECUTE'); -- yes +SELECT has_function_privilege('regress_pg_walinspect', + 'pg_get_wal_files(pg_lsn, pg_lsn)', 'EXECUTE'); -- yes REVOKE EXECUTE ON FUNCTION pg_get_wal_record_info(pg_lsn) FROM regress_pg_walinspect; @@ -152,6 +184,8 @@ REVOKE EXECUTE ON FUNCTION pg_get_wal_stats(pg_lsn, pg_lsn, boolean) FROM regress_pg_walinspect; REVOKE EXECUTE ON FUNCTION pg_get_wal_block_info(pg_lsn, pg_lsn, boolean) FROM regress_pg_walinspect; +REVOKE EXECUTE ON FUNCTION pg_get_wal_files(pg_lsn, pg_lsn) + FROM regress_pg_walinspect; -- =================================================================== -- Clean up diff --git a/contrib/pg_walinspect/t/001_timeline.pl b/contrib/pg_walinspect/t/001_timeline.pl new file mode 100644 index 00000000000..1abf7131bb7 --- /dev/null +++ b/contrib/pg_walinspect/t/001_timeline.pl @@ -0,0 +1,62 @@ +# Copyright (c) 2026, PostgreSQL Global Development Group + +# Test that pg_get_wal_files() follows the current timeline's history across +# a promotion. +use strict; +use warnings FATAL => 'all'; + +use PostgreSQL::Test::Cluster; +use PostgreSQL::Test::Utils; +use Test::More; + +sub test_timeline_history +{ + my ($suffix, $archive_mode) = @_; + my $primary = PostgreSQL::Test::Cluster->new("primary_$suffix"); + my $standby = PostgreSQL::Test::Cluster->new("standby_$suffix"); + + $primary->init(allows_streaming => 1); + $primary->append_conf('postgresql.conf', "autovacuum = off\n"); + if ($archive_mode) + { + $primary->append_conf( + 'postgresql.conf', + "archive_mode = on\narchive_command = 'true'\n"); + } + $primary->start; + $primary->safe_psql('postgres', + 'CREATE EXTENSION pg_walinspect; CREATE TABLE test_table (a int);'); + + my $backup_name = "backup_$suffix"; + $primary->backup($backup_name); + $standby->init_from_backup($primary, $backup_name, has_streaming => 1); + $standby->start; + + $primary->safe_psql('postgres', 'INSERT INTO test_table VALUES (1)'); + my $start_lsn = + $primary->safe_psql('postgres', 'SELECT pg_current_wal_insert_lsn()'); + $primary->safe_psql('postgres', 'SELECT pg_switch_wal()'); + $primary->safe_psql('postgres', 'INSERT INTO test_table VALUES (2)'); + $primary->wait_for_catchup($standby); + + $standby->promote; + $standby->safe_psql('postgres', 'INSERT INTO test_table VALUES (3)'); + + my $result = $standby->safe_psql( + 'postgres', qq{ +SELECT count(DISTINCT left(wal_file, 8)) = 2, + min(left(wal_file, 8)) = '00000001', + max(left(wal_file, 8)) = '00000002' +FROM pg_get_wal_files('$start_lsn', pg_current_wal_lsn())}); + is($result, 't|t|t', + "WAL files span the timeline switch with archive_mode=$archive_mode"); + + $standby->stop; + $primary->stop; + return; +} + +test_timeline_history('archive_off', 0); +test_timeline_history('archive_on', 1); + +done_testing(); diff --git a/doc/src/sgml/pgwalinspect.sgml b/doc/src/sgml/pgwalinspect.sgml index 79c3ead40bc..b4ecae8cfb1 100644 --- a/doc/src/sgml/pgwalinspect.sgml +++ b/doc/src/sgml/pgwalinspect.sgml @@ -34,9 +34,8 @@ - All of the pg_walinspect functions that show - information about records that fall within a certain LSN range are - permissive about accepting end_lsn + All of the pg_walinspect functions that accept an LSN + range are permissive about accepting end_lsn arguments that are after the server's current LSN. Using an end_lsn from the future will not raise an error. @@ -60,6 +59,35 @@ General Functions + + + pg_get_wal_files(start_lsn pg_lsn, end_lsn pg_lsn DEFAULT NULL) returns setof record + + + + + If end_lsn is specified and differs from + start_lsn, returns each retained WAL segment + file that intersects the half-open WAL range + [start_lsn, + end_lsn). If + end_lsn is omitted, is null, or equals + start_lsn, returns the segment containing + start_lsn. The + wal_file column contains the file name, while + segment_start_lsn and + segment_end_lsn contain the complete segment + boundaries rather than boundaries clipped to the requested range. + + + File names use the timeline valid for each segment in the current + timeline's history. This allows a returned range to cross a timeline + switch. The function reports an error if any required segment file is + not currently retained in pg_wal. + + + + pg_get_wal_record_info(in_lsn pg_lsn) returns record diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 656f1f60862..1f1bcf8d527 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -3434,6 +3434,7 @@ WalSummarizerData WalSummaryFile WalSummaryIO WalTimeSample +WalTimeSegment WalUsage WalWriteMethod WalWriteMethodOps -- 2.50.1 (Apple Git-155)