1 /*-------------------------------------------------------------------------
2  *
3  * ginvacuum.c
4  *	  delete & vacuum routines for the postgres GIN
5  *
6  *
7  * Portions Copyright (c) 1996-2016, PostgreSQL Global Development Group
8  * Portions Copyright (c) 1994, Regents of the University of California
9  *
10  * IDENTIFICATION
11  *			src/backend/access/gin/ginvacuum.c
12  *-------------------------------------------------------------------------
13  */
14 
15 #include "postgres.h"
16 
17 #include "access/gin_private.h"
18 #include "access/xloginsert.h"
19 #include "commands/vacuum.h"
20 #include "miscadmin.h"
21 #include "postmaster/autovacuum.h"
22 #include "storage/indexfsm.h"
23 #include "storage/lmgr.h"
24 #include "utils/memutils.h"
25 
26 struct GinVacuumState
27 {
28 	Relation	index;
29 	IndexBulkDeleteResult *result;
30 	IndexBulkDeleteCallback callback;
31 	void	   *callback_state;
32 	GinState	ginstate;
33 	BufferAccessStrategy strategy;
34 	MemoryContext tmpCxt;
35 };
36 
37 /*
38  * Vacuums an uncompressed posting list. The size of the must can be specified
39  * in number of items (nitems).
40  *
41  * If none of the items need to be removed, returns NULL. Otherwise returns
42  * a new palloc'd array with the remaining items. The number of remaining
43  * items is returned in *nremaining.
44  */
45 ItemPointer
ginVacuumItemPointers(GinVacuumState * gvs,ItemPointerData * items,int nitem,int * nremaining)46 ginVacuumItemPointers(GinVacuumState *gvs, ItemPointerData *items,
47 					  int nitem, int *nremaining)
48 {
49 	int			i,
50 				remaining = 0;
51 	ItemPointer tmpitems = NULL;
52 
53 	/*
54 	 * Iterate over TIDs array
55 	 */
56 	for (i = 0; i < nitem; i++)
57 	{
58 		if (gvs->callback(items + i, gvs->callback_state))
59 		{
60 			gvs->result->tuples_removed += 1;
61 			if (!tmpitems)
62 			{
63 				/*
64 				 * First TID to be deleted: allocate memory to hold the
65 				 * remaining items.
66 				 */
67 				tmpitems = palloc(sizeof(ItemPointerData) * nitem);
68 				memcpy(tmpitems, items, sizeof(ItemPointerData) * i);
69 			}
70 		}
71 		else
72 		{
73 			gvs->result->num_index_tuples += 1;
74 			if (tmpitems)
75 				tmpitems[remaining] = items[i];
76 			remaining++;
77 		}
78 	}
79 
80 	*nremaining = remaining;
81 	return tmpitems;
82 }
83 
84 /*
85  * Create a WAL record for vacuuming entry tree leaf page.
86  */
87 static void
xlogVacuumPage(Relation index,Buffer buffer)88 xlogVacuumPage(Relation index, Buffer buffer)
89 {
90 	Page		page = BufferGetPage(buffer);
91 	XLogRecPtr	recptr;
92 
93 	/* This is only used for entry tree leaf pages. */
94 	Assert(!GinPageIsData(page));
95 	Assert(GinPageIsLeaf(page));
96 
97 	if (!RelationNeedsWAL(index))
98 		return;
99 
100 	/*
101 	 * Always create a full image, we don't track the changes on the page at
102 	 * any more fine-grained level. This could obviously be improved...
103 	 */
104 	XLogBeginInsert();
105 	XLogRegisterBuffer(0, buffer, REGBUF_FORCE_IMAGE | REGBUF_STANDARD);
106 
107 	recptr = XLogInsert(RM_GIN_ID, XLOG_GIN_VACUUM_PAGE);
108 	PageSetLSN(page, recptr);
109 }
110 
111 static bool
ginVacuumPostingTreeLeaves(GinVacuumState * gvs,BlockNumber blkno,bool isRoot,Buffer * rootBuffer)112 ginVacuumPostingTreeLeaves(GinVacuumState *gvs, BlockNumber blkno, bool isRoot, Buffer *rootBuffer)
113 {
114 	Buffer		buffer;
115 	Page		page;
116 	bool		hasVoidPage = FALSE;
117 	MemoryContext oldCxt;
118 
119 	buffer = ReadBufferExtended(gvs->index, MAIN_FORKNUM, blkno,
120 								RBM_NORMAL, gvs->strategy);
121 	page = BufferGetPage(buffer);
122 
123 	/*
124 	 * We should be sure that we don't concurrent with inserts, insert process
125 	 * never release root page until end (but it can unlock it and lock
126 	 * again). New scan can't start but previously started ones work
127 	 * concurrently.
128 	 */
129 	if (isRoot)
130 		LockBufferForCleanup(buffer);
131 	else
132 		LockBuffer(buffer, GIN_EXCLUSIVE);
133 
134 	Assert(GinPageIsData(page));
135 
136 	if (GinPageIsLeaf(page))
137 	{
138 		oldCxt = MemoryContextSwitchTo(gvs->tmpCxt);
139 		ginVacuumPostingTreeLeaf(gvs->index, buffer, gvs);
140 		MemoryContextSwitchTo(oldCxt);
141 		MemoryContextReset(gvs->tmpCxt);
142 
143 		/* if root is a leaf page, we don't desire further processing */
144 		if (!isRoot && !hasVoidPage && GinDataLeafPageIsEmpty(page))
145 			hasVoidPage = TRUE;
146 	}
147 	else
148 	{
149 		OffsetNumber i;
150 		bool		isChildHasVoid = FALSE;
151 
152 		for (i = FirstOffsetNumber; i <= GinPageGetOpaque(page)->maxoff; i++)
153 		{
154 			PostingItem *pitem = GinDataPageGetPostingItem(page, i);
155 
156 			if (ginVacuumPostingTreeLeaves(gvs, PostingItemGetBlockNumber(pitem), FALSE, NULL))
157 				isChildHasVoid = TRUE;
158 		}
159 
160 		if (isChildHasVoid)
161 			hasVoidPage = TRUE;
162 	}
163 
164 	/*
165 	 * if we have root and there are empty pages in tree, then we don't
166 	 * release lock to go further processing and guarantee that tree is unused
167 	 */
168 	if (!(isRoot && hasVoidPage))
169 	{
170 		UnlockReleaseBuffer(buffer);
171 	}
172 	else
173 	{
174 		Assert(rootBuffer);
175 		*rootBuffer = buffer;
176 	}
177 
178 	return hasVoidPage;
179 }
180 
181 /*
182  * Delete a posting tree page.
183  */
184 static void
ginDeletePage(GinVacuumState * gvs,BlockNumber deleteBlkno,BlockNumber leftBlkno,BlockNumber parentBlkno,OffsetNumber myoff,bool isParentRoot)185 ginDeletePage(GinVacuumState *gvs, BlockNumber deleteBlkno, BlockNumber leftBlkno,
186 			  BlockNumber parentBlkno, OffsetNumber myoff, bool isParentRoot)
187 {
188 	Buffer		dBuffer;
189 	Buffer		lBuffer;
190 	Buffer		pBuffer;
191 	Page		page,
192 				parentPage;
193 	BlockNumber rightlink;
194 
195 	/*
196 	 * Lock the pages in the same order as an insertion would, to avoid
197 	 * deadlocks: left, then right, then parent.
198 	 */
199 	lBuffer = ReadBufferExtended(gvs->index, MAIN_FORKNUM, leftBlkno,
200 								 RBM_NORMAL, gvs->strategy);
201 	dBuffer = ReadBufferExtended(gvs->index, MAIN_FORKNUM, deleteBlkno,
202 								 RBM_NORMAL, gvs->strategy);
203 	pBuffer = ReadBufferExtended(gvs->index, MAIN_FORKNUM, parentBlkno,
204 								 RBM_NORMAL, gvs->strategy);
205 
206 	LockBuffer(lBuffer, GIN_EXCLUSIVE);
207 	LockBuffer(dBuffer, GIN_EXCLUSIVE);
208 	if (!isParentRoot)			/* parent is already locked by
209 								 * LockBufferForCleanup() */
210 		LockBuffer(pBuffer, GIN_EXCLUSIVE);
211 
212 	START_CRIT_SECTION();
213 
214 	/* Unlink the page by changing left sibling's rightlink */
215 	page = BufferGetPage(dBuffer);
216 	rightlink = GinPageGetOpaque(page)->rightlink;
217 
218 	/* For deleted page remember last xid which could knew its address */
219 	GinPageSetDeleteXid(page, ReadNewTransactionId());
220 
221 	page = BufferGetPage(lBuffer);
222 	GinPageGetOpaque(page)->rightlink = rightlink;
223 
224 	/* Delete downlink from parent */
225 	parentPage = BufferGetPage(pBuffer);
226 #ifdef USE_ASSERT_CHECKING
227 	do
228 	{
229 		PostingItem *tod = GinDataPageGetPostingItem(parentPage, myoff);
230 
231 		Assert(PostingItemGetBlockNumber(tod) == deleteBlkno);
232 	} while (0);
233 #endif
234 	GinPageDeletePostingItem(parentPage, myoff);
235 
236 	page = BufferGetPage(dBuffer);
237 
238 	/*
239 	 * we shouldn't change rightlink field to save workability of running
240 	 * search scan
241 	 */
242 	GinPageSetDeleted(page);
243 
244 	MarkBufferDirty(pBuffer);
245 	MarkBufferDirty(lBuffer);
246 	MarkBufferDirty(dBuffer);
247 
248 	if (RelationNeedsWAL(gvs->index))
249 	{
250 		XLogRecPtr	recptr;
251 		ginxlogDeletePage data;
252 
253 		/*
254 		 * We can't pass REGBUF_STANDARD for the deleted page, because we
255 		 * didn't set pd_lower on pre-9.4 versions. The page might've been
256 		 * binary-upgraded from an older version, and hence not have pd_lower
257 		 * set correctly. Ditto for the left page, but removing the item from
258 		 * the parent updated its pd_lower, so we know that's OK at this
259 		 * point.
260 		 */
261 		XLogBeginInsert();
262 		XLogRegisterBuffer(0, dBuffer, 0);
263 		XLogRegisterBuffer(1, pBuffer, REGBUF_STANDARD);
264 		XLogRegisterBuffer(2, lBuffer, 0);
265 
266 		data.parentOffset = myoff;
267 		data.rightLink = GinPageGetOpaque(page)->rightlink;
268 		data.deleteXid = GinPageGetDeleteXid(page);
269 
270 		XLogRegisterData((char *) &data, sizeof(ginxlogDeletePage));
271 
272 		recptr = XLogInsert(RM_GIN_ID, XLOG_GIN_DELETE_PAGE);
273 		PageSetLSN(page, recptr);
274 		PageSetLSN(parentPage, recptr);
275 		PageSetLSN(BufferGetPage(lBuffer), recptr);
276 	}
277 
278 	if (!isParentRoot)
279 		LockBuffer(pBuffer, GIN_UNLOCK);
280 	ReleaseBuffer(pBuffer);
281 	UnlockReleaseBuffer(lBuffer);
282 	UnlockReleaseBuffer(dBuffer);
283 
284 	END_CRIT_SECTION();
285 
286 	gvs->result->pages_deleted++;
287 }
288 
289 typedef struct DataPageDeleteStack
290 {
291 	struct DataPageDeleteStack *child;
292 	struct DataPageDeleteStack *parent;
293 
294 	BlockNumber blkno;			/* current block number */
295 	BlockNumber leftBlkno;		/* rightest non-deleted page on left */
296 	bool		isRoot;
297 } DataPageDeleteStack;
298 
299 /*
300  * scans posting tree and deletes empty pages
301  */
302 static bool
ginScanToDelete(GinVacuumState * gvs,BlockNumber blkno,bool isRoot,DataPageDeleteStack * parent,OffsetNumber myoff)303 ginScanToDelete(GinVacuumState *gvs, BlockNumber blkno, bool isRoot,
304 				DataPageDeleteStack *parent, OffsetNumber myoff)
305 {
306 	DataPageDeleteStack *me;
307 	Buffer		buffer;
308 	Page		page;
309 	bool		meDelete = FALSE;
310 	bool		isempty;
311 
312 	if (isRoot)
313 	{
314 		me = parent;
315 	}
316 	else
317 	{
318 		if (!parent->child)
319 		{
320 			me = (DataPageDeleteStack *) palloc0(sizeof(DataPageDeleteStack));
321 			me->parent = parent;
322 			parent->child = me;
323 			me->leftBlkno = InvalidBlockNumber;
324 		}
325 		else
326 			me = parent->child;
327 	}
328 
329 	buffer = ReadBufferExtended(gvs->index, MAIN_FORKNUM, blkno,
330 								RBM_NORMAL, gvs->strategy);
331 	page = BufferGetPage(buffer);
332 
333 	Assert(GinPageIsData(page));
334 
335 	if (!GinPageIsLeaf(page))
336 	{
337 		OffsetNumber i;
338 
339 		me->blkno = blkno;
340 		for (i = FirstOffsetNumber; i <= GinPageGetOpaque(page)->maxoff; i++)
341 		{
342 			PostingItem *pitem = GinDataPageGetPostingItem(page, i);
343 
344 			if (ginScanToDelete(gvs, PostingItemGetBlockNumber(pitem), FALSE, me, i))
345 				i--;
346 		}
347 	}
348 
349 	if (GinPageIsLeaf(page))
350 		isempty = GinDataLeafPageIsEmpty(page);
351 	else
352 		isempty = GinPageGetOpaque(page)->maxoff < FirstOffsetNumber;
353 
354 	if (isempty)
355 	{
356 		/* we never delete the left- or rightmost branch */
357 		if (me->leftBlkno != InvalidBlockNumber && !GinPageRightMost(page))
358 		{
359 			Assert(!isRoot);
360 			ginDeletePage(gvs, blkno, me->leftBlkno, me->parent->blkno, myoff, me->parent->isRoot);
361 			meDelete = TRUE;
362 		}
363 	}
364 
365 	ReleaseBuffer(buffer);
366 
367 	if (!meDelete)
368 		me->leftBlkno = blkno;
369 
370 	return meDelete;
371 }
372 
373 static void
ginVacuumPostingTree(GinVacuumState * gvs,BlockNumber rootBlkno)374 ginVacuumPostingTree(GinVacuumState *gvs, BlockNumber rootBlkno)
375 {
376 	Buffer		rootBuffer = InvalidBuffer;
377 	DataPageDeleteStack root,
378 			   *ptr,
379 			   *tmp;
380 
381 	if (ginVacuumPostingTreeLeaves(gvs, rootBlkno, TRUE, &rootBuffer) == FALSE)
382 	{
383 		Assert(rootBuffer == InvalidBuffer);
384 		return;
385 	}
386 
387 	memset(&root, 0, sizeof(DataPageDeleteStack));
388 	root.leftBlkno = InvalidBlockNumber;
389 	root.isRoot = TRUE;
390 
391 	vacuum_delay_point();
392 
393 	ginScanToDelete(gvs, rootBlkno, TRUE, &root, InvalidOffsetNumber);
394 
395 	ptr = root.child;
396 	while (ptr)
397 	{
398 		tmp = ptr->child;
399 		pfree(ptr);
400 		ptr = tmp;
401 	}
402 
403 	UnlockReleaseBuffer(rootBuffer);
404 }
405 
406 /*
407  * returns modified page or NULL if page isn't modified.
408  * Function works with original page until first change is occurred,
409  * then page is copied into temporary one.
410  */
411 static Page
ginVacuumEntryPage(GinVacuumState * gvs,Buffer buffer,BlockNumber * roots,uint32 * nroot)412 ginVacuumEntryPage(GinVacuumState *gvs, Buffer buffer, BlockNumber *roots, uint32 *nroot)
413 {
414 	Page		origpage = BufferGetPage(buffer),
415 				tmppage;
416 	OffsetNumber i,
417 				maxoff = PageGetMaxOffsetNumber(origpage);
418 
419 	tmppage = origpage;
420 
421 	*nroot = 0;
422 
423 	for (i = FirstOffsetNumber; i <= maxoff; i++)
424 	{
425 		IndexTuple	itup = (IndexTuple) PageGetItem(tmppage, PageGetItemId(tmppage, i));
426 
427 		if (GinIsPostingTree(itup))
428 		{
429 			/*
430 			 * store posting tree's roots for further processing, we can't
431 			 * vacuum it just now due to risk of deadlocks with scans/inserts
432 			 */
433 			roots[*nroot] = GinGetDownlink(itup);
434 			(*nroot)++;
435 		}
436 		else if (GinGetNPosting(itup) > 0)
437 		{
438 			int			nitems;
439 			ItemPointer items_orig;
440 			bool		free_items_orig;
441 			ItemPointer items;
442 
443 			/* Get list of item pointers from the tuple. */
444 			if (GinItupIsCompressed(itup))
445 			{
446 				items_orig = ginPostingListDecode((GinPostingList *) GinGetPosting(itup), &nitems);
447 				free_items_orig = true;
448 			}
449 			else
450 			{
451 				items_orig = (ItemPointer) GinGetPosting(itup);
452 				nitems = GinGetNPosting(itup);
453 				free_items_orig = false;
454 			}
455 
456 			/* Remove any items from the list that need to be vacuumed. */
457 			items = ginVacuumItemPointers(gvs, items_orig, nitems, &nitems);
458 
459 			if (free_items_orig)
460 				pfree(items_orig);
461 
462 			/* If any item pointers were removed, recreate the tuple. */
463 			if (items)
464 			{
465 				OffsetNumber attnum;
466 				Datum		key;
467 				GinNullCategory category;
468 				GinPostingList *plist;
469 				int			plistsize;
470 
471 				if (nitems > 0)
472 				{
473 					plist = ginCompressPostingList(items, nitems, GinMaxItemSize, NULL);
474 					plistsize = SizeOfGinPostingList(plist);
475 				}
476 				else
477 				{
478 					plist = NULL;
479 					plistsize = 0;
480 				}
481 
482 				/*
483 				 * if we already created a temporary page, make changes in
484 				 * place
485 				 */
486 				if (tmppage == origpage)
487 				{
488 					/*
489 					 * On first difference, create a temporary copy of the
490 					 * page and copy the tuple's posting list to it.
491 					 */
492 					tmppage = PageGetTempPageCopy(origpage);
493 
494 					/* set itup pointer to new page */
495 					itup = (IndexTuple) PageGetItem(tmppage, PageGetItemId(tmppage, i));
496 				}
497 
498 				attnum = gintuple_get_attrnum(&gvs->ginstate, itup);
499 				key = gintuple_get_key(&gvs->ginstate, itup, &category);
500 				itup = GinFormTuple(&gvs->ginstate, attnum, key, category,
501 									(char *) plist, plistsize,
502 									nitems, true);
503 				if (plist)
504 					pfree(plist);
505 				PageIndexTupleDelete(tmppage, i);
506 
507 				if (PageAddItem(tmppage, (Item) itup, IndexTupleSize(itup), i, false, false) != i)
508 					elog(ERROR, "failed to add item to index page in \"%s\"",
509 						 RelationGetRelationName(gvs->index));
510 
511 				pfree(itup);
512 				pfree(items);
513 			}
514 		}
515 	}
516 
517 	return (tmppage == origpage) ? NULL : tmppage;
518 }
519 
520 IndexBulkDeleteResult *
ginbulkdelete(IndexVacuumInfo * info,IndexBulkDeleteResult * stats,IndexBulkDeleteCallback callback,void * callback_state)521 ginbulkdelete(IndexVacuumInfo *info, IndexBulkDeleteResult *stats,
522 			  IndexBulkDeleteCallback callback, void *callback_state)
523 {
524 	Relation	index = info->index;
525 	BlockNumber blkno = GIN_ROOT_BLKNO;
526 	GinVacuumState gvs;
527 	Buffer		buffer;
528 	BlockNumber rootOfPostingTree[BLCKSZ / (sizeof(IndexTupleData) + sizeof(ItemId))];
529 	uint32		nRoot;
530 
531 	gvs.tmpCxt = AllocSetContextCreate(CurrentMemoryContext,
532 									   "Gin vacuum temporary context",
533 									   ALLOCSET_DEFAULT_SIZES);
534 	gvs.index = index;
535 	gvs.callback = callback;
536 	gvs.callback_state = callback_state;
537 	gvs.strategy = info->strategy;
538 	initGinState(&gvs.ginstate, index);
539 
540 	/* first time through? */
541 	if (stats == NULL)
542 	{
543 		/* Yes, so initialize stats to zeroes */
544 		stats = (IndexBulkDeleteResult *) palloc0(sizeof(IndexBulkDeleteResult));
545 
546 		/*
547 		 * and cleanup any pending inserts
548 		 */
549 		ginInsertCleanup(&gvs.ginstate, !IsAutoVacuumWorkerProcess(),
550 						 false, true, stats);
551 	}
552 
553 	/* we'll re-count the tuples each time */
554 	stats->num_index_tuples = 0;
555 	gvs.result = stats;
556 
557 	buffer = ReadBufferExtended(index, MAIN_FORKNUM, blkno,
558 								RBM_NORMAL, info->strategy);
559 
560 	/* find leaf page */
561 	for (;;)
562 	{
563 		Page		page = BufferGetPage(buffer);
564 		IndexTuple	itup;
565 
566 		LockBuffer(buffer, GIN_SHARE);
567 
568 		Assert(!GinPageIsData(page));
569 
570 		if (GinPageIsLeaf(page))
571 		{
572 			LockBuffer(buffer, GIN_UNLOCK);
573 			LockBuffer(buffer, GIN_EXCLUSIVE);
574 
575 			if (blkno == GIN_ROOT_BLKNO && !GinPageIsLeaf(page))
576 			{
577 				LockBuffer(buffer, GIN_UNLOCK);
578 				continue;		/* check it one more */
579 			}
580 			break;
581 		}
582 
583 		Assert(PageGetMaxOffsetNumber(page) >= FirstOffsetNumber);
584 
585 		itup = (IndexTuple) PageGetItem(page, PageGetItemId(page, FirstOffsetNumber));
586 		blkno = GinGetDownlink(itup);
587 		Assert(blkno != InvalidBlockNumber);
588 
589 		UnlockReleaseBuffer(buffer);
590 		buffer = ReadBufferExtended(index, MAIN_FORKNUM, blkno,
591 									RBM_NORMAL, info->strategy);
592 	}
593 
594 	/* right now we found leftmost page in entry's BTree */
595 
596 	for (;;)
597 	{
598 		Page		page = BufferGetPage(buffer);
599 		Page		resPage;
600 		uint32		i;
601 
602 		Assert(!GinPageIsData(page));
603 
604 		resPage = ginVacuumEntryPage(&gvs, buffer, rootOfPostingTree, &nRoot);
605 
606 		blkno = GinPageGetOpaque(page)->rightlink;
607 
608 		if (resPage)
609 		{
610 			START_CRIT_SECTION();
611 			PageRestoreTempPage(resPage, page);
612 			MarkBufferDirty(buffer);
613 			xlogVacuumPage(gvs.index, buffer);
614 			UnlockReleaseBuffer(buffer);
615 			END_CRIT_SECTION();
616 		}
617 		else
618 		{
619 			UnlockReleaseBuffer(buffer);
620 		}
621 
622 		vacuum_delay_point();
623 
624 		for (i = 0; i < nRoot; i++)
625 		{
626 			ginVacuumPostingTree(&gvs, rootOfPostingTree[i]);
627 			vacuum_delay_point();
628 		}
629 
630 		if (blkno == InvalidBlockNumber)		/* rightmost page */
631 			break;
632 
633 		buffer = ReadBufferExtended(index, MAIN_FORKNUM, blkno,
634 									RBM_NORMAL, info->strategy);
635 		LockBuffer(buffer, GIN_EXCLUSIVE);
636 	}
637 
638 	MemoryContextDelete(gvs.tmpCxt);
639 
640 	return gvs.result;
641 }
642 
643 IndexBulkDeleteResult *
ginvacuumcleanup(IndexVacuumInfo * info,IndexBulkDeleteResult * stats)644 ginvacuumcleanup(IndexVacuumInfo *info, IndexBulkDeleteResult *stats)
645 {
646 	Relation	index = info->index;
647 	bool		needLock;
648 	BlockNumber npages,
649 				blkno;
650 	BlockNumber totFreePages;
651 	GinState	ginstate;
652 	GinStatsData idxStat;
653 
654 	/*
655 	 * In an autovacuum analyze, we want to clean up pending insertions.
656 	 * Otherwise, an ANALYZE-only call is a no-op.
657 	 */
658 	if (info->analyze_only)
659 	{
660 		if (IsAutoVacuumWorkerProcess())
661 		{
662 			initGinState(&ginstate, index);
663 			ginInsertCleanup(&ginstate, false, true, true, stats);
664 		}
665 		return stats;
666 	}
667 
668 	/*
669 	 * Set up all-zero stats and cleanup pending inserts if ginbulkdelete
670 	 * wasn't called
671 	 */
672 	if (stats == NULL)
673 	{
674 		stats = (IndexBulkDeleteResult *) palloc0(sizeof(IndexBulkDeleteResult));
675 		initGinState(&ginstate, index);
676 		ginInsertCleanup(&ginstate, !IsAutoVacuumWorkerProcess(),
677 						 false, true, stats);
678 	}
679 
680 	memset(&idxStat, 0, sizeof(idxStat));
681 
682 	/*
683 	 * XXX we always report the heap tuple count as the number of index
684 	 * entries.  This is bogus if the index is partial, but it's real hard to
685 	 * tell how many distinct heap entries are referenced by a GIN index.
686 	 */
687 	stats->num_index_tuples = info->num_heap_tuples;
688 	stats->estimated_count = info->estimated_count;
689 
690 	/*
691 	 * Need lock unless it's local to this backend.
692 	 */
693 	needLock = !RELATION_IS_LOCAL(index);
694 
695 	if (needLock)
696 		LockRelationForExtension(index, ExclusiveLock);
697 	npages = RelationGetNumberOfBlocks(index);
698 	if (needLock)
699 		UnlockRelationForExtension(index, ExclusiveLock);
700 
701 	totFreePages = 0;
702 
703 	for (blkno = GIN_ROOT_BLKNO; blkno < npages; blkno++)
704 	{
705 		Buffer		buffer;
706 		Page		page;
707 
708 		vacuum_delay_point();
709 
710 		buffer = ReadBufferExtended(index, MAIN_FORKNUM, blkno,
711 									RBM_NORMAL, info->strategy);
712 		LockBuffer(buffer, GIN_SHARE);
713 		page = (Page) BufferGetPage(buffer);
714 
715 		if (GinPageIsRecyclable(page))
716 		{
717 			Assert(blkno != GIN_ROOT_BLKNO);
718 			RecordFreeIndexPage(index, blkno);
719 			totFreePages++;
720 		}
721 		else if (GinPageIsData(page))
722 		{
723 			idxStat.nDataPages++;
724 		}
725 		else if (!GinPageIsList(page))
726 		{
727 			idxStat.nEntryPages++;
728 
729 			if (GinPageIsLeaf(page))
730 				idxStat.nEntries += PageGetMaxOffsetNumber(page);
731 		}
732 
733 		UnlockReleaseBuffer(buffer);
734 	}
735 
736 	/* Update the metapage with accurate page and entry counts */
737 	idxStat.nTotalPages = npages;
738 	ginUpdateStats(info->index, &idxStat);
739 
740 	/* Finally, vacuum the FSM */
741 	IndexFreeSpaceMapVacuum(info->index);
742 
743 	stats->pages_free = totFreePages;
744 
745 	if (needLock)
746 		LockRelationForExtension(index, ExclusiveLock);
747 	stats->num_pages = RelationGetNumberOfBlocks(index);
748 	if (needLock)
749 		UnlockRelationForExtension(index, ExclusiveLock);
750 
751 	return stats;
752 }
753