On Tue, Sep 20, 2022 at 6:43 PM Robert Haas <robertmh...@gmail.com> wrote:
> On Tue, Sep 20, 2022 at 5:00 AM Himanshu Upadhyaya > <upadhyaya.himan...@gmail.com> wrote: > > Please find it attached. > > This patch still has no test cases. Just as we have test cases for the > existing corruption checks, we should have test cases for these new > corruption checks, showing cases where they actually fire. > > Test cases are now part of this v6 patch. > I think I would be inclined to set lp_valid[x] = true in both the > redirected and non-redirected case, and then have the very first thing > that the second loop does be if (nextoffnum == 0 || > !lp_valid[ctx.offnum]) continue. I think that would be more clear > about the intent to ignore line pointers that failed validation. Also, > if you did it that way, then you could catch the case of a redirected > line pointer pointing to another redirected line pointer, which is a > corruption condition that the current code does not appear to check. > > Yes, it's a good idea to do this additional validation with a redirected line pointer. Done. > + /* > + * Validation via the predecessor array. 1) If the > predecessor's > + * xmin is aborted or in progress, the current tuples xmin > should > + * be aborted or in progress respectively. Also both xmin's > must > + * be equal. 2) If the predecessor's xmin is not frozen, then > + * current tuple's shouldn't be either. 3) If the > predecessor's > + * xmin is equal to the current tuple's xmin, the current > tuple's > + * cmin should be greater than the predecessor's cmin. 4) If > the > + * current tuple is not HOT then its predecessor's tuple must > not > + * be HEAP_HOT_UPDATED. 5) If the current tuple is HOT then > its > + * predecessor's tuple must be HEAP_HOT_UPDATED. > + */ > > This comment needs to be split up into pieces and the pieces need to > be moved closer to the tests to which they correspond. > > Done. > + psprintf("unfrozen tuple was > updated to produce a tuple at offset %u which is not frozen", > Shouldn't this say "which is frozen"? > > Done. > + * Not a corruption if current tuple is updated/deleted by a > + * different transaction, means t_cid will point to cmax > (that is > + * command id of deleting transaction) and cid of predecessor > not > + * necessarily will be smaller than cid of current tuple. > t_cid > > I think that the next person who reads this code is likely to > understand that the CIDs of different transactions are numerically > unrelated. What's less obvious is that if the XID is the same, the > newer update must have a higher CID. > > + * can hold combo command id but we are not worrying here > since > + * combo command id of the next updated tuple (if present) > must be > + * greater than combo command id of the current tuple. So > here we > + * are not checking HEAP_COMBOCID flag and simply doing t_cid > + * comparison. > > I disapprove of ignoring the HEAP_COMBOCID flag. Emitting a message > claiming that the CID has a certain value when that's actually a combo > CID is misleading, so at least a different message wording is needed > in such cases. But it's also not clear to me that the newer update has > to have a higher combo CID, because combo CIDs can be reused. If you > have multiple cursors open in the same transaction, the updates can be > interleaved, and it seems to me that it might be possible for an older > CID to have created a certain combo CID after a newer CID, and then > both cursors could update the same page in succession and end up with > combo CIDs out of numerical order. Unless somebody can make a > convincing argument that this isn't possible, I think we should just > skip this check for cases where either tuple has a combo CID. > > + if (TransactionIdEquals(pred_xmin, curr_xmin) && > + (TransactionIdEquals(curr_xmin, curr_xmax) || > + !TransactionIdIsValid(curr_xmax)) && pred_cmin >= > curr_cmin) > > I don't understand the reason for the middle part of this condition -- > TransactionIdEquals(curr_xmin, curr_xmax) || > !TransactionIdIsValid(curr_xmax). I suppose the comment is meant to > explain this, but I still don't get it. If a tuple with XMIN 12345 > CMIN 2 is updated to produce a tuple with XMIN 12345 CMIN 1, that's > corruption, regardless of what the XMAX of the second tuple may happen > to be. > > As discussed in our last discussion, I am removing this check altogether. > + if (HeapTupleHeaderIsHeapOnly(curr_htup) && > + !HeapTupleHeaderIsHotUpdated(pred_htup)) > > + if (!HeapTupleHeaderIsHeapOnly(curr_htup) && > + HeapTupleHeaderIsHotUpdated(pred_htup)) > > I think it would be slightly clearer to write these tests the other > way around i.e. check the previous tuple's state first. > > Done. > + if (!TransactionIdIsValid(curr_xmax) && > HeapTupleHeaderIsHotUpdated(tuphdr)) > + { > + report_corruption(ctx, > + psprintf("tuple has been updated, but xmax is > 0")); > + result = false; > + } > > I guess this message needs to say "tuple has been HOT updated, but > xmax is 0" or something like that. > > Done. -- Regards, Himanshu Upadhyaya EnterpriseDB: http://www.enterprisedb.com
From 08fe01f5073c0a850541265494bb4a875bec7d3f Mon Sep 17 00:00:00 2001 From: Himanshu Upadhyaya <himanshu.upadhy...@enterprisedb.com> Date: Fri, 30 Sep 2022 17:44:56 +0530 Subject: [PATCH v6] Implement HOT chain validation in verify_heapam() Himanshu Upadhyaya, reviewed by Robert Haas, Aleksander Alekseev Discussion: https://postgr.es/m/CAPF61jBBR2-iE-EmN_9v0hcQEfyz_17e5Lbb0%2Bu2%3D9ukA9sWmQ%40mail.gmail.com --- contrib/amcheck/verify_heapam.c | 207 ++++++++++++++++++++++ src/bin/pg_amcheck/t/004_verify_heapam.pl | 192 ++++++++++++++++++-- 2 files changed, 388 insertions(+), 11 deletions(-) diff --git a/contrib/amcheck/verify_heapam.c b/contrib/amcheck/verify_heapam.c index c875f3e5a2..007f7b2f37 100644 --- a/contrib/amcheck/verify_heapam.c +++ b/contrib/amcheck/verify_heapam.c @@ -399,6 +399,9 @@ verify_heapam(PG_FUNCTION_ARGS) for (ctx.blkno = first_block; ctx.blkno <= last_block; ctx.blkno++) { OffsetNumber maxoff; + OffsetNumber predecessor[MaxOffsetNumber] = {0}; + OffsetNumber successor[MaxOffsetNumber] = {0}; + bool lp_valid[MaxOffsetNumber] = {false}; CHECK_FOR_INTERRUPTS(); @@ -433,6 +436,8 @@ verify_heapam(PG_FUNCTION_ARGS) for (ctx.offnum = FirstOffsetNumber; ctx.offnum <= maxoff; ctx.offnum = OffsetNumberNext(ctx.offnum)) { + OffsetNumber nextoffnum; + ctx.itemid = PageGetItemId(ctx.page, ctx.offnum); /* Skip over unused/dead line pointers */ @@ -469,6 +474,13 @@ verify_heapam(PG_FUNCTION_ARGS) report_corruption(&ctx, psprintf("line pointer redirection to unused item at offset %u", (unsigned) rdoffnum)); + + /* + * make entry in successor array, redirected tuple will be + * validated at the time when we loop over successor array + */ + successor[ctx.offnum] = rdoffnum; + lp_valid[ctx.offnum] = true; continue; } @@ -504,9 +516,197 @@ verify_heapam(PG_FUNCTION_ARGS) /* It should be safe to examine the tuple's header, at least */ ctx.tuphdr = (HeapTupleHeader) PageGetItem(ctx.page, ctx.itemid); ctx.natts = HeapTupleHeaderGetNatts(ctx.tuphdr); + lp_valid[ctx.offnum] = true; /* Ok, ready to check this next tuple */ check_tuple(&ctx); + + /* + * Add the data to the successor array if next updated tuple is in + * the same page. It will be used later to generate the + * predecessor array. + * + * We need to access the tuple's header to populate the + * predecessor array. However the tuple is not necessarily sanity + * checked yet so delaying construction of predecessor array until + * all tuples are sanity checked. + */ + nextoffnum = ItemPointerGetOffsetNumber(&(ctx.tuphdr)->t_ctid); + if (ItemPointerGetBlockNumber(&(ctx.tuphdr)->t_ctid) == ctx.blkno && + nextoffnum != ctx.offnum) + { + successor[ctx.offnum] = nextoffnum; + } + } + + /* + * Loop over offset and populate predecessor array from all entries + * that are present in successor array. + */ + ctx.attnum = -1; + for (ctx.offnum = FirstOffsetNumber; ctx.offnum <= maxoff; + ctx.offnum = OffsetNumberNext(ctx.offnum)) + { + ItemId curr_lp; + ItemId next_lp; + HeapTupleHeader curr_htup; + HeapTupleHeader next_htup; + TransactionId curr_xmax; + TransactionId next_xmin; + + OffsetNumber nextoffnum = successor[ctx.offnum]; + + curr_lp = PageGetItemId(ctx.page, ctx.offnum); + if (nextoffnum == 0 || !lp_valid[ctx.offnum] || !lp_valid[nextoffnum]) + { + /* + * This is either the last updated tuple in the chain or a + * corruption raised for this tuple. + */ + continue; + } + if (ItemIdIsRedirected(curr_lp)) + { + next_lp = PageGetItemId(ctx.page, nextoffnum); + if (ItemIdIsRedirected(next_lp)) + { + report_corruption(&ctx, + psprintf("redirected line pointer pointing to another redirected line pointer at offset %u", + (unsigned) nextoffnum)); + continue; + } + next_htup = (HeapTupleHeader) PageGetItem(ctx.page, next_lp); + if (!HeapTupleHeaderIsHeapOnly(next_htup)) + { + report_corruption(&ctx, + psprintf("redirected tuple at line pointer offset %u is not heap only tuple", + (unsigned) nextoffnum)); + } + if ((next_htup->t_infomask & HEAP_UPDATED) == 0) + { + report_corruption(&ctx, + psprintf("redirected tuple at line pointer offset %u is not heap updated tuple", + (unsigned) nextoffnum)); + } + continue; + } + + /* + * Add a line pointer offset to the predecessor array if xmax is + * matching with xmin of next tuple (reaching via its t_ctid). + * Prior to PostgreSQL 9.4, we actually changed the xmin to + * FrozenTransactionId so we must add offset to predecessor + * array(irrespective of xmax-xmin matching) if updated tuple xmin + * is frozen, so that we can later do validation related to frozen + * xmin. Raise corruption if we have two tuples having the same + * predecessor. + * + * We add the offset to the predecessor array irrespective of the + * transaction (t_xmin) status. We will do validation related to + * the transaction status (and also all other validations) when we + * loop over the predecessor array. + */ + curr_htup = (HeapTupleHeader) PageGetItem(ctx.page, curr_lp); + curr_xmax = HeapTupleHeaderGetUpdateXid(curr_htup); + + next_lp = PageGetItemId(ctx.page, nextoffnum); + next_htup = (HeapTupleHeader) PageGetItem(ctx.page, next_lp); + next_xmin = HeapTupleHeaderGetXmin(next_htup); + if (TransactionIdIsValid(curr_xmax) && + (TransactionIdEquals(curr_xmax, next_xmin) || + next_xmin == FrozenTransactionId)) + { + if (predecessor[nextoffnum] != 0) + { + report_corruption(&ctx, + psprintf("updated version at offset %u is also the updated version of tuple at offset %u", + (unsigned) nextoffnum, (unsigned) predecessor[nextoffnum])); + continue; + } + predecessor[nextoffnum] = ctx.offnum; + } + /* Non matching xmax with xmin is not a corruption */ + + } + + /* Loop over offsets and validate the data in the predecessor array. */ + for (OffsetNumber currentoffnum = FirstOffsetNumber; currentoffnum <= maxoff; + currentoffnum = OffsetNumberNext(currentoffnum)) + { + HeapTupleHeader pred_htup; + HeapTupleHeader curr_htup; + TransactionId pred_xmin; + TransactionId curr_xmin; + ItemId pred_lp; + ItemId curr_lp; + + ctx.offnum = predecessor[currentoffnum]; + ctx.attnum = -1; + + if (ctx.offnum == 0) + { + /* + * Either the root of the chain or an xmin-aborted tuple from + * an abandoned portion of the HOT chain. + */ + continue; + } + + curr_lp = PageGetItemId(ctx.page, currentoffnum); + curr_htup = (HeapTupleHeader) PageGetItem(ctx.page, curr_lp); + curr_xmin = HeapTupleHeaderGetXmin(curr_htup); + + ctx.itemid = pred_lp = PageGetItemId(ctx.page, ctx.offnum); + pred_htup = (HeapTupleHeader) PageGetItem(ctx.page, pred_lp); + pred_xmin = HeapTupleHeaderGetXmin(pred_htup); + + /* + * If the predecessor's xmin is aborted or in progress, the + * current tuples xmin should be aborted or in progress + * respectively. Also both xmin's must be equal. + */ + if (!TransactionIdEquals(pred_xmin, curr_xmin) && + !TransactionIdDidCommit(pred_xmin)) + { + report_corruption(&ctx, + psprintf("tuple with uncommitted xmin %u was updated to produce a tuple at offset %u with differing xmin %u", + (unsigned) pred_xmin, (unsigned) currentoffnum, (unsigned) curr_xmin)); + } + + /* + * If the predecessor's xmin is not frozen, then current tuple's + * shouldn't be either. + */ + if (pred_xmin != FrozenTransactionId && curr_xmin == FrozenTransactionId) + { + report_corruption(&ctx, + psprintf("unfrozen tuple was updated to produce a tuple at offset %u which is frozen", + (unsigned) currentoffnum)); + } + + /* + * If the current tuple is HOT then it's predecessor's tuple must + * be HEAP_HOT_UPDATED. + */ + if (!HeapTupleHeaderIsHotUpdated(pred_htup) && + HeapTupleHeaderIsHeapOnly(curr_htup)) + { + report_corruption(&ctx, + psprintf("non-heap-only update produced a heap-only tuple at offset %u", + (unsigned) currentoffnum)); + } + + /* + * If the current tuple is not HOT then its predecessor's tuple + * must not be HEAP_HOT_UPDATED. + */ + if (HeapTupleHeaderIsHotUpdated(pred_htup) && + !HeapTupleHeaderIsHeapOnly(curr_htup)) + { + report_corruption(&ctx, + psprintf("heap-only update produced a non-heap only tuple at offset %u", + (unsigned) currentoffnum)); + } } /* clean up */ @@ -640,6 +840,7 @@ check_tuple_header(HeapCheckContext *ctx) { HeapTupleHeader tuphdr = ctx->tuphdr; uint16 infomask = tuphdr->t_infomask; + TransactionId curr_xmax = HeapTupleHeaderGetUpdateXid(tuphdr); bool result = true; unsigned expected_hoff; @@ -651,6 +852,12 @@ check_tuple_header(HeapCheckContext *ctx) result = false; } + if (!TransactionIdIsValid(curr_xmax) && HeapTupleHeaderIsHotUpdated(tuphdr)) + { + report_corruption(ctx, + psprintf("tuple has been HOT updated, but xmax is 0")); + result = false; + } if ((ctx->tuphdr->t_infomask & HEAP_XMAX_COMMITTED) && (ctx->tuphdr->t_infomask & HEAP_XMAX_IS_MULTI)) { diff --git a/src/bin/pg_amcheck/t/004_verify_heapam.pl b/src/bin/pg_amcheck/t/004_verify_heapam.pl index bbada168f0..b026d1fcfe 100644 --- a/src/bin/pg_amcheck/t/004_verify_heapam.pl +++ b/src/bin/pg_amcheck/t/004_verify_heapam.pl @@ -174,6 +174,8 @@ sub write_tuple # Set umask so test directories and files are created with default permissions umask(0077); +my $pred_xmax; +my $aborted_xid; # Set up the node. Once we create and corrupt the table, # autovacuum workers visiting the table could crash the backend. # Disable autovacuum so that won't happen. @@ -217,7 +219,9 @@ my $rel = $node->safe_psql('postgres', my $relpath = "$pgdata/$rel"; # Insert data and freeze public.test -use constant ROWCOUNT => 16; +use constant ROWCOUNT => 33 ; # Total row count in page. +use constant ROWCOUNT_HOTCHAIN => 17; # Row count related to test of HOT chains validations and redirected LP. +# First insert data needed for non-HOT chain validation. $node->safe_psql( 'postgres', qq( INSERT INTO public.test (a, b, c) @@ -227,7 +231,37 @@ $node->safe_psql( repeat('w', 10000) ); VACUUM FREEZE public.test - )) for (1 .. ROWCOUNT); + )) for (1 .. ROWCOUNT-ROWCOUNT_HOTCHAIN); + +# Data for Redirected LP. +$node->safe_psql( + 'postgres', qq( + INSERT INTO public.test (a, b, c) + VALUES ( x'DEADF9F9DEADF9F9'::bigint, 'abcdefg', generate_series(1,2)); + UPDATE public.test SET c = 'a' WHERE c = '1'; + UPDATE public.test SET c = 'a' WHERE c = '2'; + VACUUM FREEZE public.test; + )); + +# Data for HOT chains validation, so not calling VACUUM FREEZE. +$node->safe_psql( + 'postgres', qq( + INSERT INTO public.test (a, b, c) + VALUES ( x'DEADF9F9DEADF9F9'::bigint, 'abcdefg', generate_series(3,9)); + UPDATE public.test SET c = 'a' WHERE c = '3'; + UPDATE public.test SET c = 'a' WHERE c = '6'; + UPDATE public.test SET c = 'a' WHERE c = '7'; + UPDATE public.test SET c = 'a' WHERE c = '8'; + UPDATE public.test SET c = 'a' WHERE c = '9'; + )); + +# Need one aborted transaction to test corruption in HOT chain. +$node->safe_psql( + 'postgres', qq( + BEGIN; + UPDATE public.test SET c = 'a' WHERE c = '5'; + ABORT; + )); my $relfrozenxid = $node->safe_psql('postgres', q(select relfrozenxid from pg_class where relname = 'test')); @@ -249,12 +283,21 @@ if ($datfrozenxid <= 3 || $datfrozenxid >= $relfrozenxid) my @lp_off; for my $tup (0 .. ROWCOUNT - 1) { - push( - @lp_off, - $node->safe_psql( - 'postgres', qq( -select lp_off from heap_page_items(get_raw_page('test', 'main', 0)) - offset $tup limit 1))); + my $islpredirected = $node->safe_psql('postgres', + qq(select lp_flags from heap_page_items(get_raw_page('test', 'main', 0)) offset $tup limit 1)); + if ($islpredirected != 2) + { + push( + @lp_off, + $node->safe_psql( + 'postgres', qq( + select lp_off from heap_page_items(get_raw_page('test', 'main', 0)) + offset $tup limit 1))); + } + else + { + push(@lp_off, (-1)); + } } # Sanity check that our 'test' table on disk layout matches expectations. If @@ -271,6 +314,10 @@ for (my $tupidx = 0; $tupidx < ROWCOUNT; $tupidx++) { my $offnum = $tupidx + 1; # offnum is 1-based, not zero-based my $offset = $lp_off[$tupidx]; + if ($offset == -1) + { + next; + } my $tup = read_tuple($file, $offset); # Sanity-check that the data appears on the page where we expect. @@ -283,7 +330,7 @@ for (my $tupidx = 0; $tupidx < ROWCOUNT; $tupidx++) $node->clean_node; plan skip_all => sprintf( - "Page layout differs from our expectations: expected (%x, %x, \"%s\"), got (%x, %x, \"%s\")", + "Page layout of index %d differs from our expectations: expected (%x, %x, \"%s\"), got (%x, %x, \"%s\")", $tupidx, 0xDEADF9F9, 0xDEADF9F9, "abcdefg", $a_1, $a_2, $b); exit; } @@ -318,6 +365,9 @@ use constant HEAP_XMAX_INVALID => 0x0800; use constant HEAP_NATTS_MASK => 0x07FF; use constant HEAP_XMAX_IS_MULTI => 0x1000; use constant HEAP_KEYS_UPDATED => 0x2000; +use constant HEAP_HOT_UPDATED => 0x4000; +use constant HEAP_ONLY_TUPLE => 0x8000; +use constant HEAP_UPDATED => 0x2000; # Helper function to generate a regular expression matching the header we # expect verify_heapam() to return given which fields we expect to be non-null. @@ -349,9 +399,49 @@ for (my $tupidx = 0; $tupidx < ROWCOUNT; $tupidx++) { my $offnum = $tupidx + 1; # offnum is 1-based, not zero-based my $offset = $lp_off[$tupidx]; + my $header = header(0, $offnum, undef); + # offset -1 means its redirected lp. + if ($offset == -1) + { # at offnum 19 we will unset HEAP_ONLY_TUPLE and HEAP_UPDATED flags. + if ($offnum == 17) + { + push @expected, + qr/${header}redirected tuple at line pointer offset \d+ is not heap only tuple/; + push @expected, + qr/${header}redirected tuple at line pointer offset \d+ is not heap updated tuple/; + } + elsif ($offnum == 18) + { + # we re-set lp offset to 17, we need to rewrite the 4 bytes values so that line pointer will be + # lp.off = 17, lp_flags = 2, lp_len = 0. + if ($ENDIANNESS eq 'little') + { + sysseek($file, 92, 0) + or BAIL_OUT("sysseek failed: $!"); + syswrite( + $file, + pack("L", + 0x00010011) + ) or BAIL_OUT("syswrite failed: $!"); + } + else + { + sysseek($file, 92, 0) + or BAIL_OUT("sysseek failed: $!"); + syswrite( + $file, + pack("L", + 0x11000100) + ) or BAIL_OUT("syswrite failed: $!"); + + } + push @expected, + qr/${header}redirected line pointer pointing to another redirected line pointer at offset \d+/; + } + next; + } my $tup = read_tuple($file, $offset); - my $header = header(0, $offnum, undef); if ($offnum == 1) { # Corruptly set xmin < relfrozenxid @@ -502,7 +592,7 @@ for (my $tupidx = 0; $tupidx < ROWCOUNT; $tupidx++) push @expected, qr/${header}multitransaction ID 4 equals or exceeds next valid multitransaction ID 1/; } - elsif ($offnum == 15) # Last offnum must equal ROWCOUNT + elsif ($offnum == 15) { # Set both HEAP_XMAX_COMMITTED and HEAP_XMAX_IS_MULTI $tup->{t_infomask} |= HEAP_XMAX_COMMITTED; @@ -512,6 +602,86 @@ for (my $tupidx = 0; $tupidx < ROWCOUNT; $tupidx++) push @expected, qr/${header}multitransaction ID 4000000000 precedes relation minimum multitransaction ID threshold 1/; } + # Test for redirected line pointer. + # offnum 17 and 18 are redirected line pointer, so don't need any tuple + # validation. + elsif ($offnum == 19) + { + # unset HEAP_ONLY_TUPLE and HEAP_UPDATED flag for redirected tuple. + $tup->{t_infomask2} &= ~HEAP_ONLY_TUPLE; + $tup->{t_infomask} &= ~HEAP_UPDATED; + } + # offnum 20 is redirected tuple of lp at offset 18, + # We have corrupted it to route its lp.off to point it to line pointer at + # offset 17. + + # Test related to HOT chains. + elsif ($offnum == 21) + { + # Unset HEAP_HOT_UPDATED. + $tup->{t_infomask2} &= ~HEAP_HOT_UPDATED; + $pred_xmax = $tup->{t_xmax}; # to be used for tuple at offnum 22. + push @expected, + qr/${header}non-heap-only update produced a heap-only tuple at offset \d+/; + } + elsif ($offnum == 22) + { + # Set ip_posid and t_xmax from ip_posid and t_xmax of tuple at offnum 21. + $tup->{t_xmax} = $pred_xmax; + $tup->{ip_posid} = 28; + push @expected, + qr/${header}updated version at offset \d+ is also the updated version of tuple at offset \d+/; + } + elsif ($offnum == 23) + { + # Get aborted xid, that is needed to test corruption at offnum 24. + $aborted_xid = $tup->{t_xmax}; + } + elsif ($offnum == 24) + { + # Set xmin to aborted xid. + $tup->{t_xmin} = $aborted_xid; + $tup->{t_infomask} &= ~HEAP_XMIN_COMMITTED; + $tup->{t_infomask} |= HEAP_XMIN_INVALID; + push @expected, + qr/${header}tuple with uncommitted xmin \d+ was updated to produce a tuple at offset \d+ with differing xmin \d+/; + } + elsif ($offnum == 25) + { + # Raised corruption as next updated tuple at offnum 30 is corrupted. + push @expected, + qr/${header}unfrozen tuple was updated to produce a tuple at offset \d+ which is frozen/; + } + elsif ($offnum == 26) + { + # Next updated Tuple at offnum 31 is corrupted. + push @expected, + qr/${header}heap-only update produced a non-heap only tuple at offset \d+/; + } + elsif ($offnum == 27) + { + # set xmax to invalid transaction id. + $tup->{t_xmax} = 0; + push @expected, + qr/${header}tuple has been HOT updated, but xmax is 0/; + } + # Tuple at offnum 28 is inserted by aborted transaction and we need this only + # to have one aborted XID to validate corruption for tuple at offnum 22. + # Tuple at offnum 29 is next update of tuple at offnum 22, and is tested for + # corruption related to aborted transaction. + elsif ($offnum == 30) + { + # Set xmin to FrozenTransactionId, we also set infomask to both invalid and + # committed as to match behaviour with PostgreSQL 9.4 or later). + $tup->{t_infomask} |= HEAP_XMIN_INVALID; + $tup->{t_infomask} |= HEAP_XMIN_COMMITTED; + $tup->{t_xmin} = 2; + } + elsif($offnum == 31) + { + # Unset HEAP_ONLY_TUPLE + $tup->{t_infomask2} &= ~HEAP_ONLY_TUPLE; + } write_tuple($file, $offset, $tup); } close($file) -- 2.25.1