| /*------------------------------------------------------------------------- |
| * |
| * gininsert.c |
| * insert routines for the postgres inverted index access method. |
| * |
| * |
| * Portions Copyright (c) 1996-2023, PostgreSQL Global Development Group |
| * Portions Copyright (c) 1994, Regents of the University of California |
| * |
| * IDENTIFICATION |
| * src/backend/access/gin/gininsert.c |
| *------------------------------------------------------------------------- |
| */ |
| |
| #include "postgres.h" |
| |
| #include "access/gin_private.h" |
| #include "access/ginxlog.h" |
| #include "access/tableam.h" |
| #include "access/xloginsert.h" |
| #include "catalog/index.h" |
| #include "miscadmin.h" |
| #include "storage/bufmgr.h" |
| #include "storage/indexfsm.h" |
| #include "storage/predicate.h" |
| #include "storage/smgr.h" |
| #include "utils/memutils.h" |
| #include "utils/rel.h" |
| |
| typedef struct |
| { |
| GinState ginstate; |
| double indtuples; |
| GinStatsData buildStats; |
| MemoryContext tmpCtx; |
| MemoryContext funcCtx; |
| BuildAccumulator accum; |
| } GinBuildState; |
| |
| |
| /* |
| * Adds array of item pointers to tuple's posting list, or |
| * creates posting tree and tuple pointing to tree in case |
| * of not enough space. Max size of tuple is defined in |
| * GinFormTuple(). Returns a new, modified index tuple. |
| * items[] must be in sorted order with no duplicates. |
| */ |
| static IndexTuple |
| addItemPointersToLeafTuple(GinState *ginstate, |
| IndexTuple old, |
| ItemPointerData *items, uint32 nitem, |
| GinStatsData *buildStats, Buffer buffer) |
| { |
| OffsetNumber attnum; |
| Datum key; |
| GinNullCategory category; |
| IndexTuple res; |
| ItemPointerData *newItems, |
| *oldItems; |
| int oldNPosting, |
| newNPosting; |
| GinPostingList *compressedList; |
| |
| Assert(!GinIsPostingTree(old)); |
| |
| attnum = gintuple_get_attrnum(ginstate, old); |
| key = gintuple_get_key(ginstate, old, &category); |
| |
| /* merge the old and new posting lists */ |
| oldItems = ginReadTuple(ginstate, attnum, old, &oldNPosting); |
| |
| newItems = ginMergeItemPointers(items, nitem, |
| oldItems, oldNPosting, |
| &newNPosting); |
| |
| /* Compress the posting list, and try to a build tuple with room for it */ |
| res = NULL; |
| compressedList = ginCompressPostingList(newItems, newNPosting, GinMaxItemSize, |
| NULL); |
| pfree(newItems); |
| if (compressedList) |
| { |
| res = GinFormTuple(ginstate, attnum, key, category, |
| (char *) compressedList, |
| SizeOfGinPostingList(compressedList), |
| newNPosting, |
| false); |
| pfree(compressedList); |
| } |
| if (!res) |
| { |
| /* posting list would be too big, convert to posting tree */ |
| BlockNumber postingRoot; |
| |
| /* |
| * Initialize posting tree with the old tuple's posting list. It's |
| * surely small enough to fit on one posting-tree page, and should |
| * already be in order with no duplicates. |
| */ |
| postingRoot = createPostingTree(ginstate->index, |
| oldItems, |
| oldNPosting, |
| buildStats, |
| buffer); |
| |
| /* Now insert the TIDs-to-be-added into the posting tree */ |
| ginInsertItemPointers(ginstate->index, postingRoot, |
| items, nitem, |
| buildStats); |
| |
| /* And build a new posting-tree-only result tuple */ |
| res = GinFormTuple(ginstate, attnum, key, category, NULL, 0, 0, true); |
| GinSetPostingTree(res, postingRoot); |
| } |
| pfree(oldItems); |
| |
| return res; |
| } |
| |
| /* |
| * Build a fresh leaf tuple, either posting-list or posting-tree format |
| * depending on whether the given items list will fit. |
| * items[] must be in sorted order with no duplicates. |
| * |
| * This is basically the same logic as in addItemPointersToLeafTuple, |
| * but working from slightly different input. |
| */ |
| static IndexTuple |
| buildFreshLeafTuple(GinState *ginstate, |
| OffsetNumber attnum, Datum key, GinNullCategory category, |
| ItemPointerData *items, uint32 nitem, |
| GinStatsData *buildStats, Buffer buffer) |
| { |
| IndexTuple res = NULL; |
| GinPostingList *compressedList; |
| |
| /* try to build a posting list tuple with all the items */ |
| compressedList = ginCompressPostingList(items, nitem, GinMaxItemSize, NULL); |
| if (compressedList) |
| { |
| res = GinFormTuple(ginstate, attnum, key, category, |
| (char *) compressedList, |
| SizeOfGinPostingList(compressedList), |
| nitem, false); |
| pfree(compressedList); |
| } |
| if (!res) |
| { |
| /* posting list would be too big, build posting tree */ |
| BlockNumber postingRoot; |
| |
| /* |
| * Build posting-tree-only result tuple. We do this first so as to |
| * fail quickly if the key is too big. |
| */ |
| res = GinFormTuple(ginstate, attnum, key, category, NULL, 0, 0, true); |
| |
| /* |
| * Initialize a new posting tree with the TIDs. |
| */ |
| postingRoot = createPostingTree(ginstate->index, items, nitem, |
| buildStats, buffer); |
| |
| /* And save the root link in the result tuple */ |
| GinSetPostingTree(res, postingRoot); |
| } |
| |
| return res; |
| } |
| |
| /* |
| * Insert one or more heap TIDs associated with the given key value. |
| * This will either add a single key entry, or enlarge a pre-existing entry. |
| * |
| * During an index build, buildStats is non-null and the counters |
| * it contains should be incremented as needed. |
| */ |
| void |
| ginEntryInsert(GinState *ginstate, |
| OffsetNumber attnum, Datum key, GinNullCategory category, |
| ItemPointerData *items, uint32 nitem, |
| GinStatsData *buildStats) |
| { |
| GinBtreeData btree; |
| GinBtreeEntryInsertData insertdata; |
| GinBtreeStack *stack; |
| IndexTuple itup; |
| Page page; |
| |
| insertdata.isDelete = false; |
| |
| ginPrepareEntryScan(&btree, attnum, key, category, ginstate); |
| btree.isBuild = (buildStats != NULL); |
| |
| stack = ginFindLeafPage(&btree, false, false, NULL); |
| page = BufferGetPage(stack->buffer); |
| |
| if (btree.findItem(&btree, stack)) |
| { |
| /* found pre-existing entry */ |
| itup = (IndexTuple) PageGetItem(page, PageGetItemId(page, stack->off)); |
| |
| if (GinIsPostingTree(itup)) |
| { |
| /* add entries to existing posting tree */ |
| BlockNumber rootPostingTree = GinGetPostingTree(itup); |
| |
| /* release all stack */ |
| LockBuffer(stack->buffer, GIN_UNLOCK); |
| freeGinBtreeStack(stack); |
| |
| /* insert into posting tree */ |
| ginInsertItemPointers(ginstate->index, rootPostingTree, |
| items, nitem, |
| buildStats); |
| return; |
| } |
| |
| CheckForSerializableConflictIn(ginstate->index, NULL, |
| BufferGetBlockNumber(stack->buffer)); |
| /* modify an existing leaf entry */ |
| itup = addItemPointersToLeafTuple(ginstate, itup, |
| items, nitem, buildStats, stack->buffer); |
| |
| insertdata.isDelete = true; |
| } |
| else |
| { |
| CheckForSerializableConflictIn(ginstate->index, NULL, |
| BufferGetBlockNumber(stack->buffer)); |
| /* no match, so construct a new leaf entry */ |
| itup = buildFreshLeafTuple(ginstate, attnum, key, category, |
| items, nitem, buildStats, stack->buffer); |
| |
| /* |
| * nEntries counts leaf tuples, so increment it only when we make a |
| * new one. |
| */ |
| if (buildStats) |
| buildStats->nEntries++; |
| } |
| |
| /* Insert the new or modified leaf tuple */ |
| insertdata.entry = itup; |
| ginInsertValue(&btree, stack, &insertdata, buildStats); |
| pfree(itup); |
| } |
| |
| /* |
| * Extract index entries for a single indexable item, and add them to the |
| * BuildAccumulator's state. |
| * |
| * This function is used only during initial index creation. |
| */ |
| static void |
| ginHeapTupleBulkInsert(GinBuildState *buildstate, OffsetNumber attnum, |
| Datum value, bool isNull, |
| ItemPointer heapptr) |
| { |
| Datum *entries; |
| GinNullCategory *categories; |
| int32 nentries; |
| MemoryContext oldCtx; |
| |
| oldCtx = MemoryContextSwitchTo(buildstate->funcCtx); |
| entries = ginExtractEntries(buildstate->accum.ginstate, attnum, |
| value, isNull, |
| &nentries, &categories); |
| MemoryContextSwitchTo(oldCtx); |
| |
| ginInsertBAEntries(&buildstate->accum, heapptr, attnum, |
| entries, categories, nentries); |
| |
| buildstate->indtuples += nentries; |
| |
| MemoryContextReset(buildstate->funcCtx); |
| } |
| |
| static void |
| ginBuildCallback(Relation index, ItemPointer tid, Datum *values, |
| bool *isnull, bool tupleIsAlive, void *state) |
| { |
| GinBuildState *buildstate = (GinBuildState *) state; |
| MemoryContext oldCtx; |
| int i; |
| |
| oldCtx = MemoryContextSwitchTo(buildstate->tmpCtx); |
| |
| for (i = 0; i < buildstate->ginstate.origTupdesc->natts; i++) |
| ginHeapTupleBulkInsert(buildstate, (OffsetNumber) (i + 1), |
| values[i], isnull[i], tid); |
| |
| /* If we've maxed out our available memory, dump everything to the index */ |
| if (buildstate->accum.allocatedMemory >= (Size) maintenance_work_mem * 1024L) |
| { |
| ItemPointerData *list; |
| Datum key; |
| GinNullCategory category; |
| uint32 nlist; |
| OffsetNumber attnum; |
| |
| ginBeginBAScan(&buildstate->accum); |
| while ((list = ginGetBAEntry(&buildstate->accum, |
| &attnum, &key, &category, &nlist)) != NULL) |
| { |
| /* there could be many entries, so be willing to abort here */ |
| CHECK_FOR_INTERRUPTS(); |
| ginEntryInsert(&buildstate->ginstate, attnum, key, category, |
| list, nlist, &buildstate->buildStats); |
| } |
| |
| MemoryContextReset(buildstate->tmpCtx); |
| ginInitBA(&buildstate->accum); |
| } |
| |
| MemoryContextSwitchTo(oldCtx); |
| } |
| |
| IndexBuildResult * |
| ginbuild(Relation heap, Relation index, IndexInfo *indexInfo) |
| { |
| IndexBuildResult *result; |
| double reltuples; |
| GinBuildState buildstate; |
| Buffer RootBuffer, |
| MetaBuffer; |
| ItemPointerData *list; |
| Datum key; |
| GinNullCategory category; |
| uint32 nlist; |
| MemoryContext oldCtx; |
| OffsetNumber attnum; |
| |
| if (RelationGetNumberOfBlocks(index) != 0) |
| elog(ERROR, "index \"%s\" already contains data", |
| RelationGetRelationName(index)); |
| |
| initGinState(&buildstate.ginstate, index); |
| buildstate.indtuples = 0; |
| memset(&buildstate.buildStats, 0, sizeof(GinStatsData)); |
| |
| /* initialize the meta page */ |
| MetaBuffer = GinNewBuffer(index); |
| |
| /* initialize the root page */ |
| RootBuffer = GinNewBuffer(index); |
| |
| START_CRIT_SECTION(); |
| GinInitMetabuffer(MetaBuffer); |
| MarkBufferDirty(MetaBuffer); |
| GinInitBuffer(RootBuffer, GIN_LEAF); |
| MarkBufferDirty(RootBuffer); |
| |
| |
| UnlockReleaseBuffer(MetaBuffer); |
| UnlockReleaseBuffer(RootBuffer); |
| END_CRIT_SECTION(); |
| |
| /* count the root as first entry page */ |
| buildstate.buildStats.nEntryPages++; |
| |
| /* |
| * create a temporary memory context that is used to hold data not yet |
| * dumped out to the index |
| */ |
| buildstate.tmpCtx = AllocSetContextCreate(CurrentMemoryContext, |
| "Gin build temporary context", |
| ALLOCSET_DEFAULT_SIZES); |
| |
| /* |
| * create a temporary memory context that is used for calling |
| * ginExtractEntries(), and can be reset after each tuple |
| */ |
| buildstate.funcCtx = AllocSetContextCreate(CurrentMemoryContext, |
| "Gin build temporary context for user-defined function", |
| ALLOCSET_DEFAULT_SIZES); |
| |
| buildstate.accum.ginstate = &buildstate.ginstate; |
| ginInitBA(&buildstate.accum); |
| |
| /* |
| * Do the heap scan. We disallow sync scan here because dataPlaceToPage |
| * prefers to receive tuples in TID order. |
| */ |
| reltuples = table_index_build_scan(heap, index, indexInfo, false, true, |
| ginBuildCallback, (void *) &buildstate, |
| NULL); |
| |
| /* dump remaining entries to the index */ |
| oldCtx = MemoryContextSwitchTo(buildstate.tmpCtx); |
| ginBeginBAScan(&buildstate.accum); |
| while ((list = ginGetBAEntry(&buildstate.accum, |
| &attnum, &key, &category, &nlist)) != NULL) |
| { |
| /* there could be many entries, so be willing to abort here */ |
| CHECK_FOR_INTERRUPTS(); |
| ginEntryInsert(&buildstate.ginstate, attnum, key, category, |
| list, nlist, &buildstate.buildStats); |
| } |
| MemoryContextSwitchTo(oldCtx); |
| |
| MemoryContextDelete(buildstate.funcCtx); |
| MemoryContextDelete(buildstate.tmpCtx); |
| |
| /* |
| * Update metapage stats |
| */ |
| buildstate.buildStats.nTotalPages = RelationGetNumberOfBlocks(index); |
| ginUpdateStats(index, &buildstate.buildStats, true); |
| |
| /* |
| * We didn't write WAL records as we built the index, so if WAL-logging is |
| * required, write all pages to the WAL now. |
| */ |
| if (RelationNeedsWAL(index)) |
| { |
| log_newpage_range(index, MAIN_FORKNUM, |
| 0, RelationGetNumberOfBlocks(index), |
| true); |
| } |
| |
| /* |
| * Return statistics |
| */ |
| result = (IndexBuildResult *) palloc(sizeof(IndexBuildResult)); |
| |
| result->heap_tuples = reltuples; |
| result->index_tuples = buildstate.indtuples; |
| |
| return result; |
| } |
| |
| /* |
| * ginbuildempty() -- build an empty gin index in the initialization fork |
| */ |
| void |
| ginbuildempty(Relation index) |
| { |
| Buffer RootBuffer, |
| MetaBuffer; |
| |
| /* An empty GIN index has two pages. */ |
| MetaBuffer = ExtendBufferedRel(BMR_REL(index), INIT_FORKNUM, NULL, |
| EB_LOCK_FIRST | EB_SKIP_EXTENSION_LOCK); |
| RootBuffer = ExtendBufferedRel(BMR_REL(index), INIT_FORKNUM, NULL, |
| EB_LOCK_FIRST | EB_SKIP_EXTENSION_LOCK); |
| |
| /* Initialize and xlog metabuffer and root buffer. */ |
| START_CRIT_SECTION(); |
| GinInitMetabuffer(MetaBuffer); |
| MarkBufferDirty(MetaBuffer); |
| log_newpage_buffer(MetaBuffer, true); |
| GinInitBuffer(RootBuffer, GIN_LEAF); |
| MarkBufferDirty(RootBuffer); |
| log_newpage_buffer(RootBuffer, false); |
| END_CRIT_SECTION(); |
| |
| /* Unlock and release the buffers. */ |
| UnlockReleaseBuffer(MetaBuffer); |
| UnlockReleaseBuffer(RootBuffer); |
| } |
| |
| /* |
| * Insert index entries for a single indexable item during "normal" |
| * (non-fast-update) insertion |
| */ |
| static void |
| ginHeapTupleInsert(GinState *ginstate, OffsetNumber attnum, |
| Datum value, bool isNull, |
| ItemPointer item) |
| { |
| Datum *entries; |
| GinNullCategory *categories; |
| int32 i, |
| nentries; |
| |
| entries = ginExtractEntries(ginstate, attnum, value, isNull, |
| &nentries, &categories); |
| |
| for (i = 0; i < nentries; i++) |
| ginEntryInsert(ginstate, attnum, entries[i], categories[i], |
| item, 1, NULL); |
| } |
| |
| bool |
| gininsert(Relation index, Datum *values, bool *isnull, |
| ItemPointer ht_ctid, Relation heapRel, |
| IndexUniqueCheck checkUnique, |
| bool indexUnchanged, |
| IndexInfo *indexInfo) |
| { |
| GinState *ginstate = (GinState *) indexInfo->ii_AmCache; |
| MemoryContext oldCtx; |
| MemoryContext insertCtx; |
| int i; |
| |
| /* Initialize GinState cache if first call in this statement */ |
| if (ginstate == NULL) |
| { |
| oldCtx = MemoryContextSwitchTo(indexInfo->ii_Context); |
| ginstate = (GinState *) palloc(sizeof(GinState)); |
| initGinState(ginstate, index); |
| indexInfo->ii_AmCache = (void *) ginstate; |
| MemoryContextSwitchTo(oldCtx); |
| } |
| |
| insertCtx = AllocSetContextCreate(CurrentMemoryContext, |
| "Gin insert temporary context", |
| ALLOCSET_DEFAULT_SIZES); |
| |
| oldCtx = MemoryContextSwitchTo(insertCtx); |
| |
| if (GinGetUseFastUpdate(index)) |
| { |
| GinTupleCollector collector; |
| |
| memset(&collector, 0, sizeof(GinTupleCollector)); |
| |
| for (i = 0; i < ginstate->origTupdesc->natts; i++) |
| ginHeapTupleFastCollect(ginstate, &collector, |
| (OffsetNumber) (i + 1), |
| values[i], isnull[i], |
| ht_ctid); |
| |
| ginHeapTupleFastInsert(ginstate, &collector); |
| } |
| else |
| { |
| for (i = 0; i < ginstate->origTupdesc->natts; i++) |
| ginHeapTupleInsert(ginstate, (OffsetNumber) (i + 1), |
| values[i], isnull[i], |
| ht_ctid); |
| } |
| |
| MemoryContextSwitchTo(oldCtx); |
| MemoryContextDelete(insertCtx); |
| |
| return false; |
| } |