Branch data Line data Source code
1 : : /*-------------------------------------------------------------------------
2 : : *
3 : : * heap_surgery.c
4 : : * Functions to perform surgery on the damaged heap table.
5 : : *
6 : : * Copyright (c) 2020-2026, PostgreSQL Global Development Group
7 : : *
8 : : * IDENTIFICATION
9 : : * contrib/pg_surgery/heap_surgery.c
10 : : *
11 : : *-------------------------------------------------------------------------
12 : : */
13 : : #include "postgres.h"
14 : :
15 : : #include "access/htup_details.h"
16 : : #include "access/relation.h"
17 : : #include "access/visibilitymap.h"
18 : : #include "access/xloginsert.h"
19 : : #include "catalog/pg_am_d.h"
20 : : #include "catalog/pg_control.h"
21 : : #include "miscadmin.h"
22 : : #include "storage/bufmgr.h"
23 : : #include "utils/acl.h"
24 : : #include "utils/array.h"
25 : : #include "utils/rel.h"
26 : :
27 : 1 : PG_MODULE_MAGIC_EXT(
28 : : .name = "pg_surgery",
29 : : .version = PG_VERSION
30 : : );
31 : :
32 : : /* Options to forcefully change the state of a heap tuple. */
33 : : typedef enum HeapTupleForceOption
34 : : {
35 : : HEAP_FORCE_KILL,
36 : : HEAP_FORCE_FREEZE,
37 : : } HeapTupleForceOption;
38 : :
39 : 2 : PG_FUNCTION_INFO_V1(heap_force_kill);
40 : 2 : PG_FUNCTION_INFO_V1(heap_force_freeze);
41 : :
42 : : static int32 tidcmp(const void *a, const void *b);
43 : : static Datum heap_force_common(FunctionCallInfo fcinfo,
44 : : HeapTupleForceOption heap_force_opt);
45 : : static void sanity_check_tid_array(ArrayType *ta, int *ntids);
46 : : static BlockNumber find_tids_one_page(ItemPointer tids, int ntids,
47 : : OffsetNumber *next_start_ptr);
48 : :
49 : : /*-------------------------------------------------------------------------
50 : : * heap_force_kill()
51 : : *
52 : : * Force kill the tuple(s) pointed to by the item pointer(s) stored in the
53 : : * given TID array.
54 : : *
55 : : * Usage: SELECT heap_force_kill(regclass, tid[]);
56 : : *-------------------------------------------------------------------------
57 : : */
58 : : Datum
59 : 9 : heap_force_kill(PG_FUNCTION_ARGS)
60 : : {
61 : 9 : PG_RETURN_DATUM(heap_force_common(fcinfo, HEAP_FORCE_KILL));
62 : : }
63 : :
64 : : /*-------------------------------------------------------------------------
65 : : * heap_force_freeze()
66 : : *
67 : : * Force freeze the tuple(s) pointed to by the item pointer(s) stored in the
68 : : * given TID array.
69 : : *
70 : : * Usage: SELECT heap_force_freeze(regclass, tid[]);
71 : : *-------------------------------------------------------------------------
72 : : */
73 : : Datum
74 : 7 : heap_force_freeze(PG_FUNCTION_ARGS)
75 : : {
76 : 7 : PG_RETURN_DATUM(heap_force_common(fcinfo, HEAP_FORCE_FREEZE));
77 : : }
78 : :
79 : : /*-------------------------------------------------------------------------
80 : : * heap_force_common()
81 : : *
82 : : * Common code for heap_force_kill and heap_force_freeze
83 : : *-------------------------------------------------------------------------
84 : : */
85 : : static Datum
86 : 16 : heap_force_common(FunctionCallInfo fcinfo, HeapTupleForceOption heap_force_opt)
87 : : {
88 : 16 : Oid relid = PG_GETARG_OID(0);
89 : 16 : ArrayType *ta = PG_GETARG_ARRAYTYPE_P_COPY(1);
90 : : ItemPointer tids;
91 : : int ntids,
92 : : nblocks;
93 : : Relation rel;
94 : : OffsetNumber curr_start_ptr,
95 : : next_start_ptr;
96 : : bool include_this_tid[MaxHeapTuplesPerPage];
97 : :
98 [ - + ]: 16 : if (RecoveryInProgress())
99 [ # # ]: 0 : ereport(ERROR,
100 : : (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
101 : : errmsg("recovery is in progress"),
102 : : errhint("Heap surgery functions cannot be executed during recovery.")));
103 : :
104 : : /* Check inputs. */
105 : 16 : sanity_check_tid_array(ta, &ntids);
106 : :
107 : 14 : rel = relation_open(relid, RowExclusiveLock);
108 : :
109 : : /*
110 : : * Check target relation.
111 : : */
112 [ + + + - : 14 : if (!RELKIND_HAS_TABLE_AM(rel->rd_rel->relkind))
+ + ]
113 [ + - ]: 2 : ereport(ERROR,
114 : : (errcode(ERRCODE_WRONG_OBJECT_TYPE),
115 : : errmsg("cannot operate on relation \"%s\"",
116 : : RelationGetRelationName(rel)),
117 : : errdetail_relkind_not_supported(rel->rd_rel->relkind)));
118 : :
119 [ - + ]: 12 : if (rel->rd_rel->relam != HEAP_TABLE_AM_OID)
120 [ # # ]: 0 : ereport(ERROR,
121 : : (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
122 : : errmsg("only heap AM is supported")));
123 : :
124 : : /* Must be owner of the table or superuser. */
125 [ - + ]: 12 : if (!object_ownercheck(RelationRelationId, RelationGetRelid(rel), GetUserId()))
126 : 0 : aclcheck_error(ACLCHECK_NOT_OWNER,
127 : 0 : get_relkind_objtype(rel->rd_rel->relkind),
128 : 0 : RelationGetRelationName(rel));
129 : :
130 [ - + ]: 12 : tids = ((ItemPointer) ARR_DATA_PTR(ta));
131 : :
132 : : /*
133 : : * If there is more than one TID in the array, sort them so that we can
134 : : * easily fetch all the TIDs belonging to one particular page from the
135 : : * array.
136 : : */
137 [ + + ]: 12 : if (ntids > 1)
138 : 2 : qsort(tids, ntids, sizeof(ItemPointerData), tidcmp);
139 : :
140 : 12 : curr_start_ptr = next_start_ptr = 0;
141 : 12 : nblocks = RelationGetNumberOfBlocks(rel);
142 : :
143 : : /*
144 : : * Loop, performing the necessary actions for each block.
145 : : */
146 [ + + ]: 24 : while (next_start_ptr != ntids)
147 : : {
148 : : Buffer buf;
149 : 12 : Buffer vmbuf = InvalidBuffer;
150 : 12 : bool unlock_vmbuf = false;
151 : : Page page;
152 : : BlockNumber blkno;
153 : : OffsetNumber curoff;
154 : : OffsetNumber maxoffset;
155 : : int i;
156 : 12 : bool did_modify_page = false;
157 : 12 : bool did_modify_vm = false;
158 : :
159 [ - + ]: 12 : CHECK_FOR_INTERRUPTS();
160 : :
161 : : /*
162 : : * Find all the TIDs belonging to one particular page starting from
163 : : * next_start_ptr and process them one by one.
164 : : */
165 : 12 : blkno = find_tids_one_page(tids, ntids, &next_start_ptr);
166 : :
167 : : /* Check whether the block number is valid. */
168 [ + + ]: 12 : if (blkno >= nblocks)
169 : : {
170 : : /* Update the current_start_ptr before moving to the next page. */
171 : 1 : curr_start_ptr = next_start_ptr;
172 : :
173 [ + - ]: 1 : ereport(NOTICE,
174 : : (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
175 : : errmsg("skipping block %u for relation \"%s\" because the block number is out of range",
176 : : blkno, RelationGetRelationName(rel))));
177 : 1 : continue;
178 : : }
179 : :
180 : 11 : buf = ReadBuffer(rel, blkno);
181 : 11 : LockBufferForCleanup(buf);
182 : :
183 : 11 : page = BufferGetPage(buf);
184 : :
185 : 11 : maxoffset = PageGetMaxOffsetNumber(page);
186 : :
187 : : /*
188 : : * Figure out which TIDs we are going to process and which ones we are
189 : : * going to skip.
190 : : */
191 : 11 : memset(include_this_tid, 0, sizeof(include_this_tid));
192 [ + + ]: 24 : for (i = curr_start_ptr; i < next_start_ptr; i++)
193 : : {
194 : 13 : OffsetNumber offno = ItemPointerGetOffsetNumberNoCheck(&tids[i]);
195 : : ItemId itemid;
196 : :
197 : : /* Check whether the offset number is valid. */
198 [ + + + + ]: 13 : if (offno == InvalidOffsetNumber || offno > maxoffset)
199 : : {
200 [ + - ]: 2 : ereport(NOTICE,
201 : : errmsg("skipping tid (%u, %u) for relation \"%s\" because the item number is out of range",
202 : : blkno, offno, RelationGetRelationName(rel)));
203 : 2 : continue;
204 : : }
205 : :
206 : 11 : itemid = PageGetItemId(page, offno);
207 : :
208 : : /* Only accept an item ID that is used. */
209 [ + + ]: 11 : if (ItemIdIsRedirected(itemid))
210 : : {
211 [ + - ]: 1 : ereport(NOTICE,
212 : : errmsg("skipping tid (%u, %u) for relation \"%s\" because it redirects to item %u",
213 : : blkno, offno, RelationGetRelationName(rel),
214 : : ItemIdGetRedirect(itemid)));
215 : 1 : continue;
216 : : }
217 [ + + ]: 10 : else if (ItemIdIsDead(itemid))
218 : : {
219 [ + - ]: 2 : ereport(NOTICE,
220 : : (errmsg("skipping tid (%u, %u) for relation \"%s\" because it is marked dead",
221 : : blkno, offno, RelationGetRelationName(rel))));
222 : 2 : continue;
223 : : }
224 [ + + ]: 8 : else if (!ItemIdIsUsed(itemid))
225 : : {
226 [ + - ]: 1 : ereport(NOTICE,
227 : : (errmsg("skipping tid (%u, %u) for relation \"%s\" because it is marked unused",
228 : : blkno, offno, RelationGetRelationName(rel))));
229 : 1 : continue;
230 : : }
231 : :
232 : : /* Mark it for processing. */
233 : : Assert(offno <= MaxHeapTuplesPerPage);
234 : 7 : include_this_tid[offno - 1] = true;
235 : : }
236 : :
237 : : /*
238 : : * Before entering the critical section, pin and lock the visibility
239 : : * map page if it appears to be necessary.
240 : : */
241 [ + + + + ]: 11 : if (heap_force_opt == HEAP_FORCE_KILL && PageIsAllVisible(page))
242 : : {
243 : 3 : visibilitymap_pin(rel, blkno, &vmbuf);
244 : 3 : LockBuffer(vmbuf, BUFFER_LOCK_EXCLUSIVE);
245 : 3 : unlock_vmbuf = true;
246 : : }
247 : :
248 : : /* No ereport(ERROR) from here until all the changes are logged. */
249 : 11 : START_CRIT_SECTION();
250 : :
251 [ + + ]: 55 : for (curoff = FirstOffsetNumber; curoff <= maxoffset;
252 : 44 : curoff = OffsetNumberNext(curoff))
253 : : {
254 : : ItemId itemid;
255 : :
256 [ + + ]: 44 : if (!include_this_tid[curoff - 1])
257 : 37 : continue;
258 : :
259 : 7 : itemid = PageGetItemId(page, curoff);
260 : : Assert(ItemIdIsNormal(itemid));
261 : :
262 : 7 : did_modify_page = true;
263 : :
264 [ + + ]: 7 : if (heap_force_opt == HEAP_FORCE_KILL)
265 : : {
266 : 3 : ItemIdSetDead(itemid);
267 : :
268 : : /*
269 : : * If the page is marked all-visible, we must clear
270 : : * PD_ALL_VISIBLE flag on the page header and an all-visible
271 : : * bit on the visibility map corresponding to the page.
272 : : */
273 [ + + ]: 3 : if (PageIsAllVisible(page))
274 : : {
275 [ + - ]: 1 : if (visibilitymap_clear(rel->rd_locator, blkno, vmbuf,
276 : : VISIBILITYMAP_VALID_BITS))
277 : 1 : did_modify_vm = true;
278 : :
279 : 1 : PageClearAllVisible(page);
280 : : }
281 : : }
282 : : else
283 : : {
284 : : HeapTupleHeader htup;
285 : :
286 : : Assert(heap_force_opt == HEAP_FORCE_FREEZE);
287 : :
288 : 4 : htup = (HeapTupleHeader) PageGetItem(page, itemid);
289 : :
290 : : /*
291 : : * Reset all visibility-related fields of the tuple. This
292 : : * logic should mimic heap_execute_freeze_tuple(), but we
293 : : * choose to reset xmin and ctid just to be sure that no
294 : : * potentially-garbled data is left behind.
295 : : */
296 : 4 : ItemPointerSet(&htup->t_ctid, blkno, curoff);
297 : 4 : HeapTupleHeaderSetXmin(htup, FrozenTransactionId);
298 : 4 : HeapTupleHeaderSetXmax(htup, InvalidTransactionId);
299 [ - + ]: 4 : if (htup->t_infomask & HEAP_MOVED)
300 : : {
301 [ # # ]: 0 : if (htup->t_infomask & HEAP_MOVED_OFF)
302 : 0 : HeapTupleHeaderSetXvac(htup, InvalidTransactionId);
303 : : else
304 : 0 : HeapTupleHeaderSetXvac(htup, FrozenTransactionId);
305 : : }
306 : :
307 : : /*
308 : : * Clear all the visibility-related bits of this tuple and
309 : : * mark it as frozen. Also, get rid of HOT_UPDATED and
310 : : * KEYS_UPDATES bits.
311 : : */
312 : 4 : htup->t_infomask &= ~HEAP_XACT_MASK;
313 : 4 : htup->t_infomask |= (HEAP_XMIN_FROZEN | HEAP_XMAX_INVALID);
314 : 4 : htup->t_infomask2 &= ~HEAP_HOT_UPDATED;
315 : 4 : htup->t_infomask2 &= ~HEAP_KEYS_UPDATED;
316 : : }
317 : : }
318 : :
319 : : /*
320 : : * If the page was modified, only then, we mark the buffer dirty or do
321 : : * the WAL logging.
322 : : */
323 [ + + ]: 11 : if (did_modify_page)
324 : : {
325 : : /* Mark buffer dirty before we write WAL. */
326 : 6 : MarkBufferDirty(buf);
327 : :
328 : : /* XLOG stuff */
329 [ + + - + : 6 : if (RelationNeedsWAL(rel))
- - - - ]
330 : : {
331 : : XLogRecPtr recptr;
332 : :
333 : 2 : XLogBeginInsert();
334 : 2 : XLogRegisterBuffer(0, buf, REGBUF_STANDARD | REGBUF_FORCE_IMAGE);
335 : : /* Include the VM page if it was modified */
336 [ - + ]: 2 : if (did_modify_vm)
337 : 0 : XLogRegisterBuffer(1, vmbuf, REGBUF_FORCE_IMAGE);
338 : 2 : recptr = XLogInsert(RM_XLOG_ID, XLOG_FPI);
339 [ - + ]: 2 : if (did_modify_vm)
340 : 0 : PageSetLSN(BufferGetPage(vmbuf), recptr);
341 : 2 : PageSetLSN(BufferGetPage(buf), recptr);
342 : : }
343 : : }
344 : :
345 : 11 : END_CRIT_SECTION();
346 : :
347 : 11 : UnlockReleaseBuffer(buf);
348 : :
349 [ + + ]: 11 : if (unlock_vmbuf)
350 : 3 : LockBuffer(vmbuf, BUFFER_LOCK_UNLOCK);
351 : :
352 [ + + ]: 11 : if (BufferIsValid(vmbuf))
353 : 3 : ReleaseBuffer(vmbuf);
354 : :
355 : : /* Update the current_start_ptr before moving to the next page. */
356 : 11 : curr_start_ptr = next_start_ptr;
357 : : }
358 : :
359 : 12 : relation_close(rel, RowExclusiveLock);
360 : :
361 : 12 : pfree(ta);
362 : :
363 : 12 : PG_RETURN_VOID();
364 : : }
365 : :
366 : : /*-------------------------------------------------------------------------
367 : : * tidcmp()
368 : : *
369 : : * Compare two item pointers, return -1, 0, or +1.
370 : : *
371 : : * See ItemPointerCompare for details.
372 : : * ------------------------------------------------------------------------
373 : : */
374 : : static int32
375 : 3 : tidcmp(const void *a, const void *b)
376 : : {
377 : 3 : const ItemPointerData *iptr1 = a;
378 : 3 : const ItemPointerData *iptr2 = b;
379 : :
380 : 3 : return ItemPointerCompare(iptr1, iptr2);
381 : : }
382 : :
383 : : /*-------------------------------------------------------------------------
384 : : * sanity_check_tid_array()
385 : : *
386 : : * Perform sanity checks on the given tid array, and set *ntids to the
387 : : * number of items in the array.
388 : : * ------------------------------------------------------------------------
389 : : */
390 : : static void
391 : 16 : sanity_check_tid_array(ArrayType *ta, int *ntids)
392 : : {
393 [ + + + - ]: 16 : if (ARR_HASNULL(ta) && array_contains_nulls(ta))
394 [ + - ]: 1 : ereport(ERROR,
395 : : (errcode(ERRCODE_NULL_VALUE_NOT_ALLOWED),
396 : : errmsg("array must not contain nulls")));
397 : :
398 [ + + ]: 15 : if (ARR_NDIM(ta) > 1)
399 [ + - ]: 1 : ereport(ERROR,
400 : : (errcode(ERRCODE_DATA_EXCEPTION),
401 : : errmsg("argument must be empty or one-dimensional array")));
402 : :
403 : 14 : *ntids = ArrayGetNItems(ARR_NDIM(ta), ARR_DIMS(ta));
404 : 14 : }
405 : :
406 : : /*-------------------------------------------------------------------------
407 : : * find_tids_one_page()
408 : : *
409 : : * Find all the tids residing in the same page as tids[next_start_ptr], and
410 : : * update next_start_ptr so that it points to the first tid in the next page.
411 : : *
412 : : * NOTE: The input tids[] array must be sorted.
413 : : * ------------------------------------------------------------------------
414 : : */
415 : : static BlockNumber
416 : 12 : find_tids_one_page(ItemPointer tids, int ntids, OffsetNumber *next_start_ptr)
417 : : {
418 : : int i;
419 : : BlockNumber prev_blkno,
420 : : blkno;
421 : :
422 : 12 : prev_blkno = blkno = InvalidBlockNumber;
423 : :
424 [ + + ]: 26 : for (i = *next_start_ptr; i < ntids; i++)
425 : : {
426 : 15 : ItemPointerData tid = tids[i];
427 : :
428 : 15 : blkno = ItemPointerGetBlockNumberNoCheck(&tid);
429 : :
430 [ + + ]: 15 : if (i == *next_start_ptr)
431 : 12 : prev_blkno = blkno;
432 : :
433 [ + + ]: 15 : if (prev_blkno != blkno)
434 : 1 : break;
435 : : }
436 : :
437 : 12 : *next_start_ptr = i;
438 : 12 : return prev_blkno;
439 : : }
|