Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
ee121e7
Track nesting depth correctly when drilling down into RECORD Vars.
tglsfdc Sep 15, 2023
a8f336c
Update comment about set_join_pathlist_hook().
Sep 21, 2023
238d397
Don't trust unvalidated xl_tot_len.
macdice Sep 22, 2023
10cbbb4
Don't use Perl pack('Q') in 039_end_of_wal.pl.
macdice Sep 23, 2023
dac28dc
Fix edge-case for xl_tot_len broken by bae868ca.
macdice Sep 25, 2023
bb34ca8
Backport fixup: wrap consider_join_pushdown block in joinpath.c in #i…
reshke Sep 21, 2026
3646726
Fix rowtypes expected output for bug #18077 explain plan
reshke Sep 22, 2026
0f586aa
Fail hard on out-of-memory failures in xlogreader.c
michaelpq Oct 3, 2023
e2a6f70
Refactor routine to find single log content pattern in TAP tests
michaelpq Jun 9, 2023
6b2af20
Backport fixup: relax magic number check in 039_end_of_wal split case
reshke Sep 22, 2026
15f9565
Fix incorrect step generation in HASH partition pruning
david-rowley Oct 12, 2023
bc35d4c
Fix runtime partition pruning for HASH partitioned tables
david-rowley Oct 12, 2023
7301798
Dissociate btequalimage() from interval_ops, ending its deduplication.
nmisch Oct 14, 2023
08e2588
Fix problems when a plain-inheritance parent table is excluded.
tglsfdc Oct 24, 2023
c6e0b42
Fix overflow when calculating timestamp distance in BRIN
tvondra Oct 27, 2023
2172166
Fix calculation in brin_minmax_multi_distance_date
tvondra Oct 27, 2023
a5839de
Fix minmax-multi on infinite date/timestamp values
tvondra Oct 27, 2023
9a4413c
Fix minmax-multi distance for extreme interval values
tvondra Oct 27, 2023
2340d3f
amcheck: Distinguish interrupted page deletion from corruption.
nmisch Oct 30, 2023
5dd7097
Update regression expected files to match new backports
reshke Sep 22, 2026
644e0a1
Backport fixup: port amcheck 005_pitr to PostgresNode/PG14 TAP API
reshke Sep 22, 2026
c0d5337
Mask parallel-sensitive explain output via start_ignore
reshke Sep 23, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 96 additions & 0 deletions contrib/amcheck/t/005_pitr.pl
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
# Copyright (c) 2021-2023, PostgreSQL Global Development Group

# Test integrity of intermediate states by PITR to those states
use strict;
use warnings;
use PostgresNode;
use TestLib;
use Test::More;

# origin node: generate WAL records of interest.
my $origin = PostgresNode->new('origin');
$origin->init(has_archiving => 1, allows_streaming => 1);
$origin->append_conf('postgresql.conf', 'autovacuum = off');
$origin->start;
$origin->backup('my_backup');
# Create a table with each of 6 PK values spanning 1/4 of a block. Delete the
# first four, so one index leaf is eligible for deletion. Make a replication
# slot just so pg_waldump will always have access to later WAL.
my $setup = <<EOSQL;
BEGIN;
CREATE EXTENSION amcheck;
CREATE TABLE not_leftmost (c text);
ALTER TABLE not_leftmost ALTER c SET STORAGE PLAIN;
INSERT INTO not_leftmost
-- Each value occupies roughly a quarter of a btree leaf page. This limits
-- each index tuple to fit INDEX_SIZE_MASK (8160-ish) even where the index
-- page can hold more (e.g. CBDB's 32KB pages), so leaf pages hold up to
-- four index tuples. Both this and upstream's quarter-of-8KB sizing end
-- with the same leaf layout, where deleting the first four PK values
-- leaves the leftmost leaf and one other leaf empty.
SELECT repeat(n::text, current_setting('block_size')::int / 4 - 100)
FROM generate_series(1,6) t(n);
ALTER TABLE not_leftmost ADD CONSTRAINT not_leftmost_pk PRIMARY KEY (c);
DELETE FROM not_leftmost WHERE c ~ '^[1-4]';
SELECT pg_create_physical_replication_slot('for_waldump', true, false);
COMMIT;
EOSQL
$origin->safe_psql('postgres', $setup);
my $before_vacuum_walfile =
$origin->safe_psql('postgres', "SELECT pg_walfile_name(pg_current_wal_lsn())");
# VACUUM to delete the aforementioned leaf page. Force an XLogFlush() by
# dropping a permanent table. That way, the XLogReader infrastructure can
# always see VACUUM's records, even under synchronous_commit=off. Finally,
# find the LSN of that VACUUM's last UNLINK_PAGE record.
my $vacuum = <<EOSQL;
SET synchronous_commit = off;
VACUUM (VERBOSE, INDEX_CLEANUP ON) not_leftmost;
CREATE TABLE XLogFlush ();
DROP TABLE XLogFlush;
SELECT pg_walfile_name(pg_current_wal_flush_lsn());
EOSQL
my $after_unlink_walfile = $origin->safe_psql('postgres', $vacuum);
$origin->stop;
my $unlink_lsn = do {
local %ENV = $origin->_get_env();
my $stdout;
run_log(['pg_waldump', '-p', $origin->data_dir . '/pg_wal',
$before_vacuum_walfile, $after_unlink_walfile],
'>', \$stdout);
# CBDB/GPDB may emit this record as UNLINK_PAGE_META
$stdout =~ m|^rmgr: Btree .*, lsn: ([/0-9A-F]+), .*, desc: UNLINK_PAGE(?:_META)? left|m;
$1;
};
die "did not find UNLINK_PAGE record" unless $unlink_lsn;

# replica node: amcheck at notable points in the WAL stream
my $replica = PostgresNode->new('replica');
$replica->init_from_backup($origin, 'my_backup', has_restoring => 1);
$replica->append_conf('postgresql.conf',
"recovery_target_lsn = '$unlink_lsn'");
$replica->append_conf('postgresql.conf', 'recovery_target_inclusive = off');
$replica->append_conf('postgresql.conf', 'recovery_target_action = promote');
$replica->start;
$replica->poll_query_until('postgres', "SELECT pg_is_in_recovery() = 'f';")
or die "Timed out while waiting for PITR promotion";
# recovery done; run amcheck
my $debug = "SET client_min_messages = 'debug1'";
my ($rc, $stderr);
$rc = $replica->psql(
'postgres',
"$debug; SELECT bt_index_parent_check('not_leftmost_pk', true)",
stderr => \$stderr);
print STDERR $stderr, "\n";
is($rc, 0, "bt_index_parent_check passes");
like(
$stderr,
qr/interrupted page deletion detected/,
"bt_index_parent_check: interrupted page deletion detected");
$rc = $replica->psql(
'postgres',
"$debug; SELECT bt_index_check('not_leftmost_pk', true)",
stderr => \$stderr);
print STDERR $stderr, "\n";
is($rc, 0, "bt_index_check passes");

done_testing();
96 changes: 91 additions & 5 deletions contrib/amcheck/verify_nbtree.c
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
#include "access/xact.h"
#include "catalog/index.h"
#include "catalog/pg_am.h"
#include "catalog/pg_opfamily_d.h"
#include "commands/tablecmds.h"
#include "lib/bloomfilter.h"
#include "miscadmin.h"
Expand Down Expand Up @@ -145,6 +146,9 @@ static void bt_check_every_level(Relation rel, Relation heaprel,
bool rootdescend);
static BtreeLevel bt_check_level_from_leftmost(BtreeCheckState *state,
BtreeLevel level);
static bool bt_leftmost_ignoring_half_dead(BtreeCheckState *state,
BlockNumber start,
BTPageOpaque start_opaque);
static void bt_recheck_sibling_links(BtreeCheckState *state,
BlockNumber btpo_prev_from_target,
BlockNumber leftcurrent);
Expand Down Expand Up @@ -337,10 +341,20 @@ bt_index_check_internal(Oid indrelid, bool parentcheck, bool heapallindexed,
errmsg("index \"%s\" metapage has equalimage field set on unsupported nbtree version",
RelationGetRelationName(indrel))));
if (allequalimage && !_bt_allequalimage(indrel, false))
{
bool has_interval_ops = false;

for (int i = 0; i < IndexRelationGetNumberOfKeyAttributes(indrel); i++)
if (indrel->rd_opfamily[i] == INTERVAL_BTREE_FAM_OID)
has_interval_ops = true;
ereport(ERROR,
(errcode(ERRCODE_INDEX_CORRUPTED),
errmsg("index \"%s\" metapage incorrectly indicates that deduplication is safe",
RelationGetRelationName(indrel))));
RelationGetRelationName(indrel)),
has_interval_ops
? errhint("This is known of \"interval\" indexes last built on a version predating 2023-11.")
: 0));
}

/* Check index, possibly against table it is an index on */
bt_check_every_level(indrel, heaprel, heapkeyspace, parentcheck,
Expand Down Expand Up @@ -764,7 +778,7 @@ bt_check_level_from_leftmost(BtreeCheckState *state, BtreeLevel level)
*/
if (state->readonly)
{
if (!P_LEFTMOST(opaque))
if (!bt_leftmost_ignoring_half_dead(state, current, opaque))
ereport(ERROR,
(errcode(ERRCODE_INDEX_CORRUPTED),
errmsg("block %u is not leftmost in index \"%s\"",
Expand Down Expand Up @@ -818,8 +832,16 @@ bt_check_level_from_leftmost(BtreeCheckState *state, BtreeLevel level)
*/
}

/* Sibling links should be in mutual agreement */
if (opaque->btpo_prev != leftcurrent)
/*
* Sibling links should be in mutual agreement. There arises
* leftcurrent == P_NONE && btpo_prev != P_NONE when the left sibling
* of the parent's low-key downlink is half-dead. (A half-dead page
* has no downlink from its parent.) Under heavyweight locking, the
* last bt_leftmost_ignoring_half_dead() validated this btpo_prev.
* Without heavyweight locking, validation of the P_NONE case remains
* unimplemented.
*/
if (opaque->btpo_prev != leftcurrent && leftcurrent != P_NONE)
bt_recheck_sibling_links(state, opaque->btpo_prev, leftcurrent);

/* Check level */
Expand Down Expand Up @@ -900,6 +922,66 @@ bt_check_level_from_leftmost(BtreeCheckState *state, BtreeLevel level)
return nextleveldown;
}

/*
* Like P_LEFTMOST(start_opaque), but accept an arbitrarily-long chain of
* half-dead, sibling-linked pages to the left. If a half-dead page appears
* under state->readonly, the database exited recovery between the first-stage
* and second-stage WAL records of a deletion.
*/
static bool
bt_leftmost_ignoring_half_dead(BtreeCheckState *state,
BlockNumber start,
BTPageOpaque start_opaque)
{
BlockNumber reached = start_opaque->btpo_prev,
reached_from = start;
bool all_half_dead = true;

/*
* To handle the !readonly case, we'd need to accept BTP_DELETED pages and
* potentially observe nbtree/README "Page deletion and backwards scans".
*/
Assert(state->readonly);

while (reached != P_NONE && all_half_dead)
{
Page page = palloc_btree_page(state, reached);
BTPageOpaque reached_opaque = (BTPageOpaque) PageGetSpecialPointer(page);

CHECK_FOR_INTERRUPTS();

/*
* Try to detect btpo_prev circular links. _bt_unlink_halfdead_page()
* writes that side-links will continue to point to the siblings.
* Check btpo_next for that property.
*/
all_half_dead = P_ISHALFDEAD(reached_opaque) &&
reached != start &&
reached != reached_from &&
reached_opaque->btpo_next == reached_from;
if (all_half_dead)
{
XLogRecPtr pagelsn = PageGetLSN(page);

/* pagelsn should point to an XLOG_BTREE_MARK_PAGE_HALFDEAD */
ereport(DEBUG1,
(errcode(ERRCODE_NO_DATA),
errmsg_internal("harmless interrupted page deletion detected in index \"%s\"",
RelationGetRelationName(state->rel)),
errdetail_internal("Block=%u right block=%u page lsn=%X/%X.",
reached, reached_from,
LSN_FORMAT_ARGS(pagelsn))));

reached_from = reached;
reached = reached_opaque->btpo_prev;
}

pfree(page);
}

return all_half_dead;
}

/*
* Raise an error when target page's left link does not point back to the
* previous target page, called leftcurrent here. The leftcurrent page's
Expand Down Expand Up @@ -940,6 +1022,9 @@ bt_recheck_sibling_links(BtreeCheckState *state,
BlockNumber btpo_prev_from_target,
BlockNumber leftcurrent)
{
/* taking BTPageOpaque from metapage would give irrelevant findings */
Assert(leftcurrent != P_NONE);

if (!state->readonly)
{
Buffer lbuf;
Expand Down Expand Up @@ -1923,7 +2008,8 @@ bt_child_highkey_check(BtreeCheckState *state,
opaque = (BTPageOpaque) PageGetSpecialPointer(page);

/* The first page we visit at the level should be leftmost */
if (first && !BlockNumberIsValid(state->prevrightlink) && !P_LEFTMOST(opaque))
if (first && !BlockNumberIsValid(state->prevrightlink) &&
!bt_leftmost_ignoring_half_dead(state, blkno, opaque))
ereport(ERROR,
(errcode(ERRCODE_INDEX_CORRUPTED),
errmsg("the first child of leftmost target page is not leftmost of its level in index \"%s\"",
Expand Down
3 changes: 2 additions & 1 deletion contrib/pax_storage/src/test/regress/expected/opr_sanity.out
Original file line number Diff line number Diff line change
Expand Up @@ -2231,6 +2231,7 @@ ORDER BY 1, 2, 3;
| complex_ops | complex_ops | complex
| float_ops | float4_ops | real
| float_ops | float8_ops | double precision
| interval_ops | interval_ops | interval
| jsonb_ops | jsonb_ops | jsonb
| multirange_ops | multirange_ops | anymultirange
| numeric_ops | numeric_ops | numeric
Expand All @@ -2239,7 +2240,7 @@ ORDER BY 1, 2, 3;
| record_ops | record_ops | record
| tsquery_ops | tsquery_ops | tsquery
| tsvector_ops | tsvector_ops | tsvector
(16 rows)
(17 rows)

-- **************** pg_index ****************
-- Look for illegal values in pg_index fields.
Expand Down
46 changes: 10 additions & 36 deletions src/backend/access/brin/brin_minmax_multi.c
Original file line number Diff line number Diff line change
Expand Up @@ -2075,13 +2075,15 @@ brin_minmax_multi_distance_uuid(PG_FUNCTION_ARGS)
Datum
brin_minmax_multi_distance_date(PG_FUNCTION_ARGS)
{
float8 delta = 0;
DateADT dateVal1 = PG_GETARG_DATEADT(0);
DateADT dateVal2 = PG_GETARG_DATEADT(1);

if (DATE_NOT_FINITE(dateVal1) || DATE_NOT_FINITE(dateVal2))
PG_RETURN_FLOAT8(0);
delta = (float8) dateVal2 - (float8) dateVal1;

Assert(delta >= 0);

PG_RETURN_FLOAT8(dateVal1 - dateVal2);
PG_RETURN_FLOAT8(delta);
}

/*
Expand Down Expand Up @@ -2135,10 +2137,7 @@ brin_minmax_multi_distance_timestamp(PG_FUNCTION_ARGS)
Timestamp dt1 = PG_GETARG_TIMESTAMP(0);
Timestamp dt2 = PG_GETARG_TIMESTAMP(1);

if (TIMESTAMP_NOT_FINITE(dt1) || TIMESTAMP_NOT_FINITE(dt2))
PG_RETURN_FLOAT8(0);

delta = dt2 - dt1;
delta = (float8) dt2 - (float8) dt1;

Assert(delta >= 0);

Expand All @@ -2155,45 +2154,20 @@ brin_minmax_multi_distance_interval(PG_FUNCTION_ARGS)

Interval *ia = PG_GETARG_INTERVAL_P(0);
Interval *ib = PG_GETARG_INTERVAL_P(1);
Interval *result;

int64 dayfraction;
int64 days;

result = (Interval *) palloc(sizeof(Interval));

result->month = ib->month - ia->month;
/* overflow check copied from int4mi */
if (!SAMESIGN(ib->month, ia->month) &&
!SAMESIGN(result->month, ib->month))
ereport(ERROR,
(errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE),
errmsg("interval out of range")));

result->day = ib->day - ia->day;
if (!SAMESIGN(ib->day, ia->day) &&
!SAMESIGN(result->day, ib->day))
ereport(ERROR,
(errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE),
errmsg("interval out of range")));

result->time = ib->time - ia->time;
if (!SAMESIGN(ib->time, ia->time) &&
!SAMESIGN(result->time, ib->time))
ereport(ERROR,
(errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE),
errmsg("interval out of range")));

/*
* Delta is (fractional) number of days between the intervals. Assume
* months have 30 days for consistency with interval_cmp_internal. We
* don't need to be exact, in the worst case we'll build a bit less
* efficient ranges. But we should not contradict interval_cmp.
*/
dayfraction = result->time % USECS_PER_DAY;
days = result->time / USECS_PER_DAY;
days += result->month * INT64CONST(30);
days += result->day;
dayfraction = (ib->time % USECS_PER_DAY) - (ia->time % USECS_PER_DAY);
days = (ib->time / USECS_PER_DAY) - (ia->time / USECS_PER_DAY);
days += (int64) ib->day - (int64) ia->day;
days += ((int64) ib->month - (int64) ia->month) * INT64CONST(30);

/* convert to double precision */
delta = (double) days + dayfraction / (double) USECS_PER_DAY;
Expand Down
Loading
Loading