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