=== Applying patches on top of PostgreSQL commit ID 83032dc42cab67dfdd33152d08434785977f069f === /etc/rc.d/jail: WARNING: Per-jail configuration via jail_* variables is obsolete. Please consider migrating to /etc/jail.conf. Thu Sep 3 21:06:29 UTC 2026 On branch cf/5444 nothing to commit, working tree clean === using 'git am' to apply patch ./v15-0001-using-indexscan-to-speedup-add-not-null-constraints.patch === Applying: using indexscan to speedup add not null constraints Using index info to reconstruct a base tree... M src/backend/commands/tablecmds.c M src/test/modules/test_misc/meson.build Falling back to patching base and 3-way merge... Auto-merging src/test/modules/test_misc/meson.build CONFLICT (content): Merge conflict in src/test/modules/test_misc/meson.build Auto-merging src/backend/commands/tablecmds.c CONFLICT (content): Merge conflict in src/backend/commands/tablecmds.c error: Failed to merge in the changes. hint: Use 'git am --show-current-patch=diff' to see the failed patch Patch failed at 0001 using indexscan to speedup add not null constraints When you have resolved this problem, run "git am --continue". If you prefer to skip this patch, run "git am --skip" instead. To restore the original branch and stop patching, run "git am --abort". === using patch(1) to apply patch ./v15-0001-using-indexscan-to-speedup-add-not-null-constraints.patch === patching file src/backend/commands/tablecmds.c Hunk #1 succeeded at 212 with fuzz 1 (offset -1 lines). Hunk #2 FAILED at 797. Hunk #3 succeeded at 6265 (offset -50 lines). Hunk #4 succeeded at 6295 (offset -50 lines). Hunk #5 succeeded at 6377 (offset -50 lines). Hunk #6 succeeded at 6395 (offset -50 lines). Hunk #7 succeeded at 8086 (offset -56 lines). Hunk #8 succeeded at 8215 (offset -56 lines). Hunk #9 succeeded at 10140 (offset -50 lines). Hunk #10 succeeded at 13817 (offset -51 lines). Hunk #11 succeeded at 13882 with fuzz 1 (offset -51 lines). Hunk #12 succeeded at 22954 with fuzz 2 (offset -1554 lines). 1 out of 12 hunks FAILED -- saving rejects to file src/backend/commands/tablecmds.c.rej patching file src/test/modules/test_misc/meson.build Hunk #1 FAILED at 23. 1 out of 1 hunk FAILED -- saving rejects to file src/test/modules/test_misc/meson.build.rej patching file src/test/modules/test_misc/t/015_indexscan_validate_notnull.pl Unstaged changes after reset: M src/backend/commands/tablecmds.c Removing src/backend/commands/tablecmds.c.rej Removing src/test/modules/test_misc/meson.build.rej Removing src/test/modules/test_misc/t/015_indexscan_validate_notnull.pl === using 'git apply' to apply patch ./v15-0001-using-indexscan-to-speedup-add-not-null-constraints.patch === Applied patch to 'src/backend/commands/tablecmds.c' with conflicts. Applied patch to 'src/test/modules/test_misc/meson.build' with conflicts. Falling back to direct application... U src/backend/commands/tablecmds.c U src/test/modules/test_misc/meson.build diff --cc src/backend/commands/tablecmds.c index fd144d783d9,9e4e557d29d..00000000000 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@@ -209,11 -211,12 +209,13 @@@ typedef struct AlteredTableInf char *clusterOnIndex; /* index to use for CLUSTER */ List *changedStatisticsOids; /* OIDs of statistics to rebuild */ List *changedStatisticsDefs; /* string definitions of same */ + List *changedStatisticsOwners; /* owners of same */ } AlteredTableInfo; - /* Struct describing one new constraint to check in Phase 3 scan */ - /* Note: new not-null constraints are handled elsewhere */ + /* + * Struct describing one new constraint to check in Phase 3 scan. Note: new + * not-null constraints were also added to Phase3. + */ typedef struct NewConstraint { char *name; /* Constraint name, or NULL if none */ @@@ -764,6 -789,15 +767,18 @@@ static void ATDetachCheckNoForeignKeyRe static char GetAttributeCompression(Oid atttypid, const char *compression); static char GetAttributeStorage(Oid atttypid, const char *storagemode); ++<<<<<<< ours ++======= + static void ATExecMergePartitions(List **wqueue, AlteredTableInfo *tab, Relation rel, + PartitionCmd *cmd, AlterTableUtilityContext *context); + static void ATExecSplitPartition(List **wqueue, AlteredTableInfo *tab, + Relation rel, PartitionCmd *cmd, + AlterTableUtilityContext *context); + static List *collectPartitionIndexExtDeps(List *partitionOids); + static void applyPartitionIndexExtDeps(Oid newPartOid, List *extDepState); + static void freePartitionIndexExtDeps(List *extDepState); + static bool index_check_notnull(Relation relation, List *notnull_attnums); ++>>>>>>> theirs /* ---------------------------------------------------------------- * DefineRelation @@@ -13775,9 -13931,23 +13891,23 @@@ QueueNNConstraintValidation(List **wque } /* Set attnotnull appropriately without queueing another validation */ - set_attnotnull(NULL, rel, attnum, true, false); + set_attnotnull(NULL, rel, attnum, false); tab = ATGetQueueEntry(wqueue, rel); + + /* + * Queue validation for phase 3. ALTER TABLE SET NOT NULL adds a NOT NULL + * constraint (NewConstraint) to AlteredTableInfo->constraints; for + * consistency, we do the same here. This setup allows phase 3 + * (ATRewriteTable) to quickly validate the column's NULL status using an + * index scan, if all conditions are met. + */ + newcon = palloc0_object(NewConstraint); + newcon->name = colname; + newcon->contype = CONSTR_NOTNULL; + newcon->attnum = attnum; + tab->constraints = lappend(tab->constraints, newcon); + tab->verify_new_notnull = true; /* @@@ -22836,3 -22976,1712 +22966,1715 @@@ GetAttributeStorage(Oid atttypid, cons return cstorage; } ++<<<<<<< ours ++======= + + /* + * buildExpressionExecutionStates: build the needed expression execution states + * for new partition (newPartRel) checks and initialize expressions for + * generated columns. All expressions should be created in "tab" + * (AlteredTableInfo structure). + */ + static void + buildExpressionExecutionStates(AlteredTableInfo *tab, Relation newPartRel, EState *estate) + { + /* + * Build the needed expression execution states. Here, we expect only NOT + * NULL and CHECK constraint. + */ + foreach_ptr(NewConstraint, con, tab->constraints) + { + switch (con->contype) + { + case CONSTR_CHECK: + + /* + * We already expanded virtual expression in + * createTableConstraints. + */ + con->qualstate = ExecPrepareExpr((Expr *) con->qual, estate); + break; + case CONSTR_NOTNULL: + /* Nothing to do here. */ + break; + default: + elog(ERROR, "unrecognized constraint type: %d", + (int) con->contype); + } + } + + /* Expression already planned in createTableConstraints */ + foreach_ptr(NewColumnValue, ex, tab->newvals) + ex->exprstate = ExecInitExpr((Expr *) ex->expr, NULL); + } + + /* + * evaluateGeneratedExpressionsAndCheckConstraints: evaluate any generated + * expressions for "tab" (AlteredTableInfo structure) whose inputs come from + * the new tuple (insertslot) of the new partition (newPartRel). + */ + static void + evaluateGeneratedExpressionsAndCheckConstraints(AlteredTableInfo *tab, + Relation newPartRel, + TupleTableSlot *insertslot, + ExprContext *econtext) + { + econtext->ecxt_scantuple = insertslot; + + foreach_ptr(NewColumnValue, ex, tab->newvals) + { + if (!ex->is_generated) + continue; + + insertslot->tts_values[ex->attnum - 1] + = ExecEvalExpr(ex->exprstate, + econtext, + &insertslot->tts_isnull[ex->attnum - 1]); + } + + foreach_ptr(NewConstraint, con, tab->constraints) + { + switch (con->contype) + { + case CONSTR_CHECK: + if (!ExecCheck(con->qualstate, econtext)) + ereport(ERROR, + errcode(ERRCODE_CHECK_VIOLATION), + errmsg("check constraint \"%s\" of relation \"%s\" is violated by some row", + con->name, RelationGetRelationName(newPartRel)), + errtableconstraint(newPartRel, con->name)); + break; + case CONSTR_NOTNULL: + case CONSTR_FOREIGN: + /* Nothing to do here */ + break; + default: + elog(ERROR, "unrecognized constraint type: %d", + (int) con->contype); + } + } + } + + /* + * getAttributesList: build a list of columns (ColumnDef) based on parent_rel + */ + static List * + getAttributesList(Relation parent_rel) + { + AttrNumber parent_attno; + TupleDesc modelDesc; + List *colList = NIL; + + modelDesc = RelationGetDescr(parent_rel); + + for (parent_attno = 1; parent_attno <= modelDesc->natts; + parent_attno++) + { + Form_pg_attribute attribute = TupleDescAttr(modelDesc, + parent_attno - 1); + ColumnDef *def; + + /* Ignore dropped columns in the parent. */ + if (attribute->attisdropped) + continue; + + def = makeColumnDef(NameStr(attribute->attname), attribute->atttypid, + attribute->atttypmod, attribute->attcollation); + + def->is_not_null = attribute->attnotnull; + + /* Copy identity. */ + def->identity = attribute->attidentity; + + /* Copy attgenerated. */ + def->generated = attribute->attgenerated; + + def->storage = attribute->attstorage; + + /* Likewise, copy compression. */ + if (CompressionMethodIsValid(attribute->attcompression)) + def->compression = + pstrdup(GetCompressionMethodName(attribute->attcompression)); + else + def->compression = NULL; + + /* Add to column list. */ + colList = lappend(colList, def); + } + + return colList; + } + + /* + * createTableConstraints: + * create check constraints, default values, and generated values for newRel + * based on parent_rel. tab is pending-work queue for newRel, we may need it in + * MergePartitionsMoveRows. + */ + static void + createTableConstraints(List **wqueue, AlteredTableInfo *tab, + Relation parent_rel, Relation newRel) + { + TupleDesc tupleDesc; + TupleConstr *constr; + AttrMap *attmap; + AttrNumber parent_attno; + int ccnum; + List *constraints = NIL; + List *cookedConstraints = NIL; + + tupleDesc = RelationGetDescr(parent_rel); + constr = tupleDesc->constr; + + if (!constr) + return; + + /* + * Construct a map from the parent relation's attnos to the child rel's. + * This re-checks type match, etc, although it shouldn't be possible to + * have a failure since both tables are locked. + */ + attmap = build_attrmap_by_name(RelationGetDescr(newRel), + tupleDesc, + false); + + /* Cycle for default values. */ + for (parent_attno = 1; parent_attno <= tupleDesc->natts; parent_attno++) + { + Form_pg_attribute attribute = TupleDescAttr(tupleDesc, + parent_attno - 1); + + /* Ignore dropped columns in the parent. */ + if (attribute->attisdropped) + continue; + + /* Copy the default, if present, and it should be copied. */ + if (attribute->atthasdef) + { + Node *this_default = NULL; + bool found_whole_row; + AttrNumber num; + Node *def; + NewColumnValue *newval; + + if (attribute->attgenerated == ATTRIBUTE_GENERATED_VIRTUAL) + this_default = build_generation_expression(parent_rel, attribute->attnum); + else + { + this_default = TupleDescGetDefault(tupleDesc, attribute->attnum); + if (this_default == NULL) + elog(ERROR, "default expression not found for attribute %d of relation \"%s\"", + attribute->attnum, RelationGetRelationName(parent_rel)); + } + + num = attmap->attnums[parent_attno - 1]; + def = map_variable_attnos(this_default, 1, 0, attmap, InvalidOid, &found_whole_row); + + if (found_whole_row && attribute->attgenerated != '\0') + elog(ERROR, "cannot convert whole-row table reference"); + + /* Add a pre-cooked default expression. */ + StoreAttrDefault(newRel, num, def, false); + + /* + * Stored generated column expressions in parent_rel might + * reference the tableoid. newRel, parent_rel tableoid clear is + * not the same. If so, these stored generated columns require + * recomputation for newRel within MergePartitionsMoveRows. + */ + if (attribute->attgenerated == ATTRIBUTE_GENERATED_STORED) + { + newval = palloc0_object(NewColumnValue); + newval->attnum = num; + newval->expr = expression_planner((Expr *) def); + newval->is_generated = (attribute->attgenerated != '\0'); + tab->newvals = lappend(tab->newvals, newval); + } + } + } + + /* Cycle for CHECK constraints. */ + for (ccnum = 0; ccnum < constr->num_check; ccnum++) + { + char *ccname = constr->check[ccnum].ccname; + char *ccbin = constr->check[ccnum].ccbin; + bool ccenforced = constr->check[ccnum].ccenforced; + bool ccnoinherit = constr->check[ccnum].ccnoinherit; + bool ccvalid = constr->check[ccnum].ccvalid; + Node *ccbin_node; + bool found_whole_row; + Constraint *con; + + /* + * The partitioned table can not have a NO INHERIT check constraint + * (see StoreRelCheck function for details). + */ + Assert(!ccnoinherit); + + ccbin_node = map_variable_attnos(stringToNode(ccbin), + 1, 0, + attmap, + InvalidOid, &found_whole_row); + + /* + * For the moment we have to reject whole-row variables (as for CREATE + * TABLE LIKE and inheritances). + */ + if (found_whole_row) + elog(ERROR, "Constraint \"%s\" contains a whole-row reference to table \"%s\".", + ccname, + RelationGetRelationName(parent_rel)); + + con = makeNode(Constraint); + con->contype = CONSTR_CHECK; + con->conname = pstrdup(ccname); + con->deferrable = false; + con->initdeferred = false; + con->is_enforced = ccenforced; + con->skip_validation = !ccvalid; + con->initially_valid = ccvalid; + con->is_no_inherit = ccnoinherit; + con->raw_expr = NULL; + con->cooked_expr = nodeToString(ccbin_node); + con->location = -1; + constraints = lappend(constraints, con); + } + + /* Install all CHECK constraints. */ + cookedConstraints = AddRelationNewConstraints(newRel, NIL, constraints, + false, true, false, NULL); + + /* Make the additional catalog changes visible. */ + CommandCounterIncrement(); + + /* + * parent_rel check constraint expression may reference tableoid, so later + * in MergePartitionsMoveRows, we need to evaluate the check constraint + * again for the newRel. We can check whether the check constraint + * contains a tableoid reference via pull_varattnos. + */ + foreach_ptr(CookedConstraint, ccon, cookedConstraints) + { + if (!ccon->skip_validation) + { + Node *qual; + Bitmapset *attnums = NULL; + + Assert(ccon->contype == CONSTR_CHECK); + qual = expand_generated_columns_in_expr(ccon->expr, newRel, 1); + pull_varattnos(qual, 1, &attnums); + + /* + * Add a check only if it contains a tableoid + * (TableOidAttributeNumber). + */ + if (bms_is_member(TableOidAttributeNumber - FirstLowInvalidHeapAttributeNumber, + attnums)) + { + NewConstraint *newcon; + + newcon = palloc0_object(NewConstraint); + newcon->name = ccon->name; + newcon->contype = CONSTR_CHECK; + newcon->qual = qual; + + tab->constraints = lappend(tab->constraints, newcon); + } + } + } + + /* Don't need the cookedConstraints anymore. */ + list_free_deep(cookedConstraints); + + /* Reproduce not-null constraints. */ + if (constr->has_not_null) + { + List *nnconstraints; + + /* + * The "include_noinh" argument is false because a partitioned table + * can't have NO INHERIT constraint. + */ + nnconstraints = RelationGetNotNullConstraints(RelationGetRelid(parent_rel), + false, false); + + Assert(list_length(nnconstraints) > 0); + + /* + * We already set pg_attribute.attnotnull in createPartitionTable. No + * need call set_attnotnull again. + */ + AddRelationNewConstraints(newRel, NIL, nnconstraints, false, true, false, NULL); + } + } + + /* + * createPartitionTable: + * + * Create a new partition (newPartName) for the partitioned table (parent_rel). + * ownerId is determined by the partition on which the operation is performed, + * so it is passed separately. The new partition will inherit the access method + * and persistence type from the parent table. + * + * Returns the created relation (locked in AccessExclusiveLock mode). + */ + static Relation + createPartitionTable(List **wqueue, RangeVar *newPartName, + Relation parent_rel, Oid ownerId) + { + Relation newRel; + Oid newRelId; + Oid existingRelid; + Oid tablespaceId; + TupleDesc descriptor; + List *colList = NIL; + Oid relamId; + Oid namespaceId; + AlteredTableInfo *new_partrel_tab; + Form_pg_class parent_relform = parent_rel->rd_rel; + + /* If the existing rel is temp, it must belong to this session. */ + if (RELATION_IS_OTHER_TEMP(parent_rel)) + ereport(ERROR, + errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("cannot create as partition of temporary relation of another session")); + + /* Look up inheritance ancestors and generate the relation schema. */ + colList = getAttributesList(parent_rel); + + /* Create a tuple descriptor from the relation schema. */ + descriptor = BuildDescForRelation(colList); + + /* Look up the access method for the new relation. */ + relamId = (parent_relform->relam != InvalidOid) ? parent_relform->relam : HEAP_TABLE_AM_OID; + + /* Look up the namespace in which we are supposed to create the relation. */ + namespaceId = + RangeVarGetAndCheckCreationNamespace(newPartName, NoLock, &existingRelid); + if (OidIsValid(existingRelid)) + ereport(ERROR, + errcode(ERRCODE_DUPLICATE_TABLE), + errmsg("relation \"%s\" already exists", newPartName->relname)); + + /* + * We intended to create the partition with the same persistence as the + * parent table, but we still need to recheck because that might be + * affected by the search_path. If the parent is permanent, so must be + * all of its partitions. + */ + if (parent_relform->relpersistence != RELPERSISTENCE_TEMP && + newPartName->relpersistence == RELPERSISTENCE_TEMP) + ereport(ERROR, + errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("cannot create a temporary relation as partition of permanent relation \"%s\"", + RelationGetRelationName(parent_rel))); + + /* Permanent rels cannot be partitions belonging to a temporary parent. */ + if (newPartName->relpersistence != RELPERSISTENCE_TEMP && + parent_relform->relpersistence == RELPERSISTENCE_TEMP) + ereport(ERROR, + errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("cannot create a permanent relation as partition of temporary relation \"%s\"", + RelationGetRelationName(parent_rel))); + + /* + * Select the tablespace for the new partition. Mirror the logic that + * CREATE TABLE foo PARTITION OF ... uses in DefineRelation: take the + * partitioned parent's explicit tablespace if it has one, otherwise take + * default_tablespace into account, and finally use the database default. + */ + tablespaceId = parent_relform->reltablespace; + if (!OidIsValid(tablespaceId)) + tablespaceId = GetDefaultTablespace(newPartName->relpersistence, false); + + /* Check permissions except when using database's default */ + if (OidIsValid(tablespaceId) && tablespaceId != MyDatabaseTableSpace) + { + AclResult aclresult; + + aclresult = object_aclcheck(TableSpaceRelationId, tablespaceId, + GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tablespaceId)); + } + + /* In all cases disallow placing user relations in pg_global */ + if (tablespaceId == GLOBALTABLESPACE_OID) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("only shared relations can be placed in pg_global tablespace"))); + + /* Create the relation. */ + newRelId = heap_create_with_catalog(newPartName->relname, + namespaceId, + tablespaceId, + InvalidOid, + InvalidOid, + InvalidOid, + ownerId, + relamId, + descriptor, + NIL, + RELKIND_RELATION, + newPartName->relpersistence, + false, + false, + ONCOMMIT_NOOP, + (Datum) 0, + true, + allowSystemTableMods, + false, /* is_internal */ + InvalidOid, + NULL); + + /* + * We must bump the command counter to make the newly-created relation + * tuple visible for opening. + */ + CommandCounterIncrement(); + + /* + * Create a TOAST table if the table needs one. MERGE/SPLIT PARTITION + * moves rows from existing partition(s) into new partition(s), which may + * carry out-of-line varlena values that the new relation must be able to + * store. Also, the new partition must be able to receive out-of-line + * varlena values after the DDL operation is complete. + */ + NewRelationCreateToastTable(newRelId, (Datum) 0); + + /* + * Open the new partition with no lock, because we already have an + * AccessExclusiveLock placed there after creation. + */ + newRel = table_open(newRelId, NoLock); + + /* Find or create a work queue entry for the newly created table. */ + new_partrel_tab = ATGetQueueEntry(wqueue, newRel); + + /* Create constraints, default values, and generated values. */ + createTableConstraints(wqueue, new_partrel_tab, parent_rel, newRel); + + /* + * Need to call CommandCounterIncrement, so a fresh relcache entry has + * newly installed constraint info. + */ + CommandCounterIncrement(); + + return newRel; + } + + /* + * MergePartitionsMoveRows: scan partitions to be merged (mergingPartitions) + * of the partitioned table and move rows into the new partition + * (newPartRel). We also verify check constraints against these rows. + */ + static void + MergePartitionsMoveRows(List **wqueue, List *mergingPartitions, Relation newPartRel) + { + CommandId mycid; + EState *estate; + AlteredTableInfo *tab; + ListCell *ltab; + + /* The FSM is empty, so don't bother using it. */ + uint32 ti_options = TABLE_INSERT_SKIP_FSM; + BulkInsertState bistate; /* state of bulk inserts for partition */ + TupleTableSlot *dstslot; + + /* Find the work queue entry for the new partition table: newPartRel. */ + tab = ATGetQueueEntry(wqueue, newPartRel); + + /* Generate the constraint and default execution states. */ + estate = CreateExecutorState(); + + buildExpressionExecutionStates(tab, newPartRel, estate); + + mycid = GetCurrentCommandId(true); + + /* Prepare a BulkInsertState for table_tuple_insert. */ + bistate = GetBulkInsertState(); + + /* Create the necessary tuple slot. */ + dstslot = table_slot_create(newPartRel, NULL); + + foreach_oid(merging_oid, mergingPartitions) + { + ExprContext *econtext; + TupleTableSlot *srcslot; + TupleConversionMap *tuple_map; + TableScanDesc scan; + MemoryContext oldCxt; + Snapshot snapshot; + Relation mergingPartition; + + econtext = GetPerTupleExprContext(estate); + + /* + * Partition is already locked in the transformPartitionCmdForMerge + * function. + */ + mergingPartition = table_open(merging_oid, NoLock); + + /* Create a source tuple slot for the partition being merged. */ + srcslot = table_slot_create(mergingPartition, NULL); + + /* + * Map computing for moving attributes of the merged partition to the + * new partition. + */ + tuple_map = convert_tuples_by_name(RelationGetDescr(mergingPartition), + RelationGetDescr(newPartRel)); + + /* Scan through the rows. */ + snapshot = RegisterSnapshot(GetLatestSnapshot()); + scan = table_beginscan(mergingPartition, snapshot, 0, NULL, + SO_NONE); + + /* + * Switch to per-tuple memory context and reset it for each tuple + * produced, so we don't leak memory. + */ + oldCxt = MemoryContextSwitchTo(GetPerTupleMemoryContext(estate)); + + while (table_scan_getnextslot(scan, ForwardScanDirection, srcslot)) + { + TupleTableSlot *insertslot; + + CHECK_FOR_INTERRUPTS(); + + if (tuple_map) + { + /* Need to use a map to copy attributes. */ + insertslot = execute_attr_map_slot(tuple_map->attrMap, srcslot, dstslot); + } + else + { + slot_getallattrs(srcslot); + + /* Copy attributes directly. */ + insertslot = dstslot; + + ExecClearTuple(insertslot); + + memcpy(insertslot->tts_values, srcslot->tts_values, + sizeof(Datum) * srcslot->tts_nvalid); + memcpy(insertslot->tts_isnull, srcslot->tts_isnull, + sizeof(bool) * srcslot->tts_nvalid); + + ExecStoreVirtualTuple(insertslot); + } + + /* + * Constraints and GENERATED expressions might reference the + * tableoid column, so fill tts_tableOid with the desired value. + * (We must do this each time, because it gets overwritten with + * newrel's OID during storing.) + */ + insertslot->tts_tableOid = RelationGetRelid(newPartRel); + + /* + * Now, evaluate any generated expressions whose inputs come from + * the new tuple. We assume these columns won't reference each + * other, so that there's no ordering dependency. + */ + evaluateGeneratedExpressionsAndCheckConstraints(tab, newPartRel, + insertslot, econtext); + + /* Write the tuple out to the new relation. */ + table_tuple_insert(newPartRel, insertslot, mycid, + ti_options, bistate); + + ResetExprContext(econtext); + } + + MemoryContextSwitchTo(oldCxt); + table_endscan(scan); + UnregisterSnapshot(snapshot); + + if (tuple_map) + free_conversion_map(tuple_map); + + ExecDropSingleTupleTableSlot(srcslot); + table_close(mergingPartition, NoLock); + } + + FreeExecutorState(estate); + ExecDropSingleTupleTableSlot(dstslot); + FreeBulkInsertState(bistate); + + table_finish_bulk_insert(newPartRel, ti_options); + + /* + * We don't need to process this newPartRel since we already processed it + * here, so delete the ALTER TABLE queue for it. + */ + foreach(ltab, *wqueue) + { + tab = (AlteredTableInfo *) lfirst(ltab); + if (tab->relid == RelationGetRelid(newPartRel)) + { + *wqueue = list_delete_cell(*wqueue, ltab); + break; + } + } + } + + /* + * detachPartitionTable: detach partition "child_rel" from partitioned table + * "parent_rel" with default partition identifier "defaultPartOid" + */ + static void + detachPartitionTable(Relation parent_rel, Relation child_rel, Oid defaultPartOid) + { + /* Remove the pg_inherits row first. */ + RemoveInheritance(child_rel, parent_rel, false); + + /* + * Detaching the partition might involve TOAST table access, so ensure we + * have a valid snapshot. + */ + PushActiveSnapshot(GetTransactionSnapshot()); + + /* Do the final part of detaching. */ + DetachPartitionFinalize(parent_rel, child_rel, false, defaultPartOid); + + PopActiveSnapshot(); + } + + /* + * equal_oid_lists: return true if two OID lists, each sorted in ascending + * order, contain the same OIDs in the same order. + */ + static bool + equal_oid_lists(const List *a, const List *b) + { + ListCell *la, + *lb; + + if (list_length(a) != list_length(b)) + return false; + + forboth(la, a, lb, b) + { + if (lfirst_oid(la) != lfirst_oid(lb)) + return false; + } + return true; + } + + /* + * Comparator for list_sort() on a list of PartitionIndexExtDepEntry *. + * Orders by parentIndexOid, then by indexOid as a tiebreaker so conflict + * reports for different parent indexes are deterministic. + */ + static int + cmp_partition_index_ext_dep(const ListCell *a, const ListCell *b) + { + const PartitionIndexExtDepEntry *ea = lfirst(a); + const PartitionIndexExtDepEntry *eb = lfirst(b); + + if (ea->parentIndexOid != eb->parentIndexOid) + return pg_cmp_u32(ea->parentIndexOid, eb->parentIndexOid); + return pg_cmp_u32(ea->indexOid, eb->indexOid); + } + + /* + * collectPartitionIndexExtDeps: collect extension dependencies from indexes + * on the given partitions. + * + * For each partition index that has a parent partitioned index, we collect + * extension dependencies. All source partition indexes sharing the same + * parent partitioned index must depend on exactly the same set of + * extensions; otherwise an error is raised so that we neither silently drop + * nor silently add dependencies on the merged partition's index. + * + * Indexes that don't have a parent partitioned index (i.e., indexes created + * directly on a partition without a corresponding parent index) are skipped. + * + * The returned list is sorted by parentIndexOid with exactly one entry per + * parent partitioned index, so applyPartitionIndexExtDeps() can scan it + * linearly. + */ + static List * + collectPartitionIndexExtDeps(List *partitionOids) + { + List *collected = NIL; + List *result = NIL; + PartitionIndexExtDepEntry *prev = NULL; + + /* + * Phase 1: collect one entry per (partition index -> parent index) pair, + * with its extension dependency OIDs sorted ascending. + */ + foreach_oid(partOid, partitionOids) + { + Relation partRel; + List *indexList; + + /* + * Use NoLock since the caller already holds AccessExclusiveLock on + * these partitions. + */ + partRel = table_open(partOid, NoLock); + indexList = RelationGetIndexList(partRel); + + foreach_oid(indexOid, indexList) + { + Oid parentIndexOid; + PartitionIndexExtDepEntry *entry; + + if (!get_rel_relispartition(indexOid)) + continue; + + parentIndexOid = get_partition_parent(indexOid, true); + if (!OidIsValid(parentIndexOid)) + continue; + + entry = palloc(sizeof(PartitionIndexExtDepEntry)); + entry->parentIndexOid = parentIndexOid; + entry->indexOid = indexOid; + entry->extensionOids = getAutoExtensionsOfObject(RelationRelationId, + indexOid); + list_sort(entry->extensionOids, list_oid_cmp); + + collected = lappend(collected, entry); + } + + list_free(indexList); + table_close(partRel, NoLock); + } + + /* + * Phase 2: sort by parentIndexOid so entries sharing a parent index sit + * adjacent. + */ + list_sort(collected, cmp_partition_index_ext_dep); + + /* + * Phase 3: single linear pass verifying that adjacent entries sharing a + * parent index have identical extension dependencies, and keeping one + * representative entry per parent index. + */ + foreach_ptr(PartitionIndexExtDepEntry, entry, collected) + { + if (prev != NULL && prev->parentIndexOid == entry->parentIndexOid) + { + if (!equal_oid_lists(prev->extensionOids, entry->extensionOids)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot merge partitions with conflicting extension dependencies"), + errdetail("Partition indexes \"%s\" and \"%s\" depend on different extensions.", + get_rel_name(prev->indexOid), + get_rel_name(entry->indexOid)))); + + /* Duplicate entry for the same parent index; discard. */ + list_free(entry->extensionOids); + pfree(entry); + continue; + } + + result = lappend(result, entry); + prev = entry; + } + + list_free(collected); + + return result; + } + + /* + * applyPartitionIndexExtDeps: apply collected extension dependencies to + * indexes on a new partition. + * + * For each index on the new partition, look up its parent index in the + * extDepState list. If found, record extension dependencies on the new index. + * extDepState is sorted by parentIndexOid, so the inner scan can bail out + * as soon as it passes the target OID. + */ + static void + applyPartitionIndexExtDeps(Oid newPartOid, List *extDepState) + { + Relation partRel; + List *indexList; + + if (extDepState == NIL) + return; + + /* + * Use NoLock since the caller already holds AccessExclusiveLock on the + * new partition. + */ + partRel = table_open(newPartOid, NoLock); + indexList = RelationGetIndexList(partRel); + + foreach_oid(indexOid, indexList) + { + Oid parentIdxOid; + + if (!get_rel_relispartition(indexOid)) + continue; + + parentIdxOid = get_partition_parent(indexOid, true); + if (!OidIsValid(parentIdxOid)) + continue; + + foreach_ptr(PartitionIndexExtDepEntry, entry, extDepState) + { + ObjectAddress indexAddr; + + if (entry->parentIndexOid > parentIdxOid) + break; + if (entry->parentIndexOid < parentIdxOid) + continue; + + ObjectAddressSet(indexAddr, RelationRelationId, indexOid); + + foreach_oid(extOid, entry->extensionOids) + { + ObjectAddress extAddr; + + ObjectAddressSet(extAddr, ExtensionRelationId, extOid); + recordDependencyOn(&indexAddr, &extAddr, + DEPENDENCY_AUTO_EXTENSION); + } + break; + } + } + + list_free(indexList); + table_close(partRel, NoLock); + } + + /* + * freePartitionIndexExtDeps: free memory allocated by collectPartitionIndexExtDeps. + */ + static void + freePartitionIndexExtDeps(List *extDepState) + { + foreach_ptr(PartitionIndexExtDepEntry, entry, extDepState) + { + list_free(entry->extensionOids); + pfree(entry); + } + list_free(extDepState); + } + + /* + * ALTER TABLE MERGE PARTITIONS INTO + */ + static void + ATExecMergePartitions(List **wqueue, AlteredTableInfo *tab, Relation rel, + PartitionCmd *cmd, AlterTableUtilityContext *context) + { + Relation newPartRel; + List *mergingPartitions = NIL; + List *extDepState = NIL; + Oid defaultPartOid; + Oid existingRelid; + Oid ownerId = InvalidOid; + Oid save_userid; + int save_sec_context; + int save_nestlevel; + + /* + * Check ownership of merged partitions - partitions with different owners + * cannot be merged. Also, collect the OIDs of these partitions during the + * check. + */ + foreach_node(RangeVar, name, cmd->partlist) + { + Relation mergingPartition; + + /* + * We are going to detach and remove this partition. We already took + * AccessExclusiveLock lock on transformPartitionCmdForMerge, so here, + * NoLock is fine. + */ + mergingPartition = table_openrv_extended(name, NoLock, false); + Assert(CheckRelationLockedByMe(mergingPartition, AccessExclusiveLock, false)); + + if (OidIsValid(ownerId)) + { + /* Do the partitions being merged have different owners? */ + if (ownerId != mergingPartition->rd_rel->relowner) + ereport(ERROR, + errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("partitions being merged have different owners")); + } + else + ownerId = mergingPartition->rd_rel->relowner; + + /* Store the next merging partition into the list. */ + mergingPartitions = lappend_oid(mergingPartitions, + RelationGetRelid(mergingPartition)); + + table_close(mergingPartition, NoLock); + } + + /* Look up the existing relation by the new partition name. */ + RangeVarGetAndCheckCreationNamespace(cmd->name, NoLock, &existingRelid); + + /* + * Check if this name is already taken. This helps us to detect the + * situation when one of the merging partitions has the same name as the + * new partition. Otherwise, this would fail later on anyway, but + * catching this here allows us to emit a nicer error message. + */ + if (OidIsValid(existingRelid)) + { + if (list_member_oid(mergingPartitions, existingRelid)) + { + /* + * The new partition has the same name as one of the merging + * partitions. + */ + char tmpRelName[NAMEDATALEN]; + + /* Generate a temporary name. */ + sprintf(tmpRelName, "merge-%u-%X-tmp", RelationGetRelid(rel), MyProcPid); + + /* + * Rename the existing partition with a temporary name, leaving it + * free for the new partition. We don't need to care about this + * in the future because we're going to eventually drop the + * existing partition anyway. + */ + RenameRelationInternal(existingRelid, tmpRelName, true, false); + + /* + * We must bump the command counter to make the new partition + * tuple visible for rename. + */ + CommandCounterIncrement(); + } + else + { + ereport(ERROR, + errcode(ERRCODE_DUPLICATE_TABLE), + errmsg("relation \"%s\" already exists", cmd->name->relname)); + } + } + + defaultPartOid = + get_default_oid_from_partdesc(RelationGetPartitionDesc(rel, true)); + + /* + * Collect extension dependencies from indexes on the merging partitions. + * We must do this before detaching them, so we can restore the + * dependencies on the new partition's indexes later. + */ + extDepState = collectPartitionIndexExtDeps(mergingPartitions); + + /* Detach all merging partitions. */ + foreach_oid(mergingPartitionOid, mergingPartitions) + { + Relation child_rel; + + child_rel = table_open(mergingPartitionOid, NoLock); + + detachPartitionTable(rel, child_rel, defaultPartOid); + + table_close(child_rel, NoLock); + } + + /* + * Perform a preliminary check to determine whether it's safe to drop all + * merging partitions before we actually do so later. After merging rows + * into the new partitions via MergePartitionsMoveRows, all old partitions + * need to be dropped. However, since the drop behavior is DROP_RESTRICT + * and the merge process (MergePartitionsMoveRows) can be time-consuming, + * performing an early check on the drop eligibility of old partitions is + * preferable. + */ + foreach_oid(mergingPartitionOid, mergingPartitions) + { + ObjectAddress object; + + /* Get oid of the later to be dropped relation. */ + object.objectId = mergingPartitionOid; + object.classId = RelationRelationId; + object.objectSubId = 0; + + performDeletionCheck(&object, DROP_RESTRICT, 0); + } + + /* + * Create a table for the new partition, using the partitioned table as a + * model. + */ + Assert(OidIsValid(ownerId)); + newPartRel = createPartitionTable(wqueue, cmd->name, rel, ownerId); + + /* + * Switch to the table owner's userid, so that any index functions are run + * as that user. Also, lockdown security-restricted operations and + * arrange to make GUC variable changes local to this command. + * + * Need to do it after determining the namespace in the + * createPartitionTable() call. + */ + GetUserIdAndSecContext(&save_userid, &save_sec_context); + SetUserIdAndSecContext(ownerId, + save_sec_context | SECURITY_RESTRICTED_OPERATION); + save_nestlevel = NewGUCNestLevel(); + RestrictSearchPath(); + + /* Copy data from merged partitions to the new partition. */ + MergePartitionsMoveRows(wqueue, mergingPartitions, newPartRel); + + /* Drop the current partitions before attaching the new one. */ + foreach_oid(mergingPartitionOid, mergingPartitions) + { + ObjectAddress object; + + object.objectId = mergingPartitionOid; + object.classId = RelationRelationId; + object.objectSubId = 0; + + performDeletion(&object, DROP_RESTRICT, 0); + } + + list_free(mergingPartitions); + + /* + * Attach a new partition to the partitioned table. wqueue = NULL: + * verification for each cloned constraint is not needed. + */ + attachPartitionTable(NULL, rel, newPartRel, cmd->bound); + + /* + * Apply extension dependencies to the new partition's indexes. This + * preserves any "DEPENDS ON EXTENSION" settings from the merged + * partitions. + */ + applyPartitionIndexExtDeps(RelationGetRelid(newPartRel), extDepState); + + freePartitionIndexExtDeps(extDepState); + + /* Keep the lock until commit. */ + table_close(newPartRel, NoLock); + + /* Roll back any GUC changes executed by index functions. */ + AtEOXact_GUC(false, save_nestlevel); + + /* Restore the userid and security context. */ + SetUserIdAndSecContext(save_userid, save_sec_context); + } + + /* + * Struct with the context of the new partition for inserting rows from the + * split partition. + */ + typedef struct SplitPartitionContext + { + ExprState *partqualstate; /* expression for checking a slot for a + * partition (NULL for DEFAULT partition) */ + BulkInsertState bistate; /* state of bulk inserts for partition */ + TupleTableSlot *dstslot; /* slot for inserting row into partition */ + AlteredTableInfo *tab; /* structure with generated column expressions + * and check constraint expressions. */ + Relation partRel; /* relation for partition */ + } SplitPartitionContext; + + /* + * createSplitPartitionContext: create context for partition and fill it + */ + static SplitPartitionContext * + createSplitPartitionContext(Relation partRel) + { + SplitPartitionContext *pc; + + pc = palloc0_object(SplitPartitionContext); + pc->partRel = partRel; + + /* + * Prepare a BulkInsertState for table_tuple_insert. The FSM is empty, so + * don't bother using it. + */ + pc->bistate = GetBulkInsertState(); + + /* Create a destination tuple slot for the new partition. */ + pc->dstslot = table_slot_create(pc->partRel, NULL); + + return pc; + } + + /* + * deleteSplitPartitionContext: delete context for partition + */ + static void + deleteSplitPartitionContext(SplitPartitionContext *pc, List **wqueue, uint32 ti_options) + { + ListCell *ltab; + + ExecDropSingleTupleTableSlot(pc->dstslot); + FreeBulkInsertState(pc->bistate); + + table_finish_bulk_insert(pc->partRel, ti_options); + + /* + * We don't need to process this pc->partRel so delete the ALTER TABLE + * queue of it. + */ + foreach(ltab, *wqueue) + { + AlteredTableInfo *tab = (AlteredTableInfo *) lfirst(ltab); + + if (tab->relid == RelationGetRelid(pc->partRel)) + { + *wqueue = list_delete_cell(*wqueue, ltab); + break; + } + } + + pfree(pc); + } + + /* + * SplitPartitionMoveRows: scan split partition (splitRel) of partitioned table + * (rel) and move rows into new partitions. + * + * New partitions description: + * partlist: list of pointers to SinglePartitionSpec structures. It contains + * the partition specification details for all new partitions. + * newPartRels: list of Relations, new partitions created in + * ATExecSplitPartition. + */ + static void + SplitPartitionMoveRows(List **wqueue, Relation rel, Relation splitRel, + List *partlist, List *newPartRels) + { + /* The FSM is empty, so don't bother using it. */ + uint32 ti_options = TABLE_INSERT_SKIP_FSM; + CommandId mycid; + EState *estate; + ListCell *listptr, + *listptr2; + TupleTableSlot *srcslot; + ExprContext *econtext; + TableScanDesc scan; + Snapshot snapshot; + MemoryContext oldCxt; + List *partContexts = NIL; + TupleConversionMap *tuple_map; + SplitPartitionContext *defaultPartCtx = NULL, + *pc; + + mycid = GetCurrentCommandId(true); + + estate = CreateExecutorState(); + + forboth(listptr, partlist, listptr2, newPartRels) + { + SinglePartitionSpec *sps = (SinglePartitionSpec *) lfirst(listptr); + + pc = createSplitPartitionContext((Relation) lfirst(listptr2)); + + /* Find the work queue entry for the new partition table: newPartRel. */ + pc->tab = ATGetQueueEntry(wqueue, pc->partRel); + + buildExpressionExecutionStates(pc->tab, pc->partRel, estate); + + if (sps->bound->is_default) + { + /* + * We should not create a structure to check the partition + * constraint for the new DEFAULT partition. + */ + defaultPartCtx = pc; + } + else + { + List *partConstraint; + + /* Build expression execution states for partition check quals. */ + partConstraint = get_qual_from_partbound(rel, sps->bound); + partConstraint = + (List *) eval_const_expressions(NULL, + (Node *) partConstraint); + /* Make a boolean expression for ExecCheck(). */ + partConstraint = list_make1(make_ands_explicit(partConstraint)); + + /* + * Map the vars in the constraint expression from rel's attnos to + * splitRel's. + */ + partConstraint = map_partition_varattnos(partConstraint, + 1, splitRel, rel); + + pc->partqualstate = + ExecPrepareExpr((Expr *) linitial(partConstraint), estate); + Assert(pc->partqualstate != NULL); + } + + /* Store partition context into a list. */ + partContexts = lappend(partContexts, pc); + } + + econtext = GetPerTupleExprContext(estate); + + /* Create the necessary tuple slot. */ + srcslot = table_slot_create(splitRel, NULL); + + /* + * Map computing for moving attributes of the split partition to the new + * partition (for the first new partition, but other new partitions can + * use the same map). + */ + pc = (SplitPartitionContext *) lfirst(list_head(partContexts)); + tuple_map = convert_tuples_by_name(RelationGetDescr(splitRel), + RelationGetDescr(pc->partRel)); + + /* Scan through the rows. */ + snapshot = RegisterSnapshot(GetLatestSnapshot()); + scan = table_beginscan(splitRel, snapshot, 0, NULL, + SO_NONE); + + /* + * Switch to per-tuple memory context and reset it for each tuple + * produced, so we don't leak memory. + */ + oldCxt = MemoryContextSwitchTo(GetPerTupleMemoryContext(estate)); + + while (table_scan_getnextslot(scan, ForwardScanDirection, srcslot)) + { + bool found = false; + TupleTableSlot *insertslot; + + CHECK_FOR_INTERRUPTS(); + + econtext->ecxt_scantuple = srcslot; + + /* Search partition for the current slot, srcslot. */ + foreach(listptr, partContexts) + { + pc = (SplitPartitionContext *) lfirst(listptr); + + /* skip DEFAULT partition */ + if (pc->partqualstate && ExecCheck(pc->partqualstate, econtext)) + { + found = true; + break; + } + } + if (!found) + { + /* Use the DEFAULT partition if it exists. */ + if (defaultPartCtx) + pc = defaultPartCtx; + else + ereport(ERROR, + errcode(ERRCODE_CHECK_VIOLATION), + errmsg("cannot find partition for split partition row"), + errtable(splitRel)); + } + + if (tuple_map) + { + /* Need to use a map to copy attributes. */ + insertslot = execute_attr_map_slot(tuple_map->attrMap, srcslot, pc->dstslot); + } + else + { + /* Extract data from the old tuple. */ + slot_getallattrs(srcslot); + + /* Copy attributes directly. */ + insertslot = pc->dstslot; + + ExecClearTuple(insertslot); + + memcpy(insertslot->tts_values, srcslot->tts_values, + sizeof(Datum) * srcslot->tts_nvalid); + memcpy(insertslot->tts_isnull, srcslot->tts_isnull, + sizeof(bool) * srcslot->tts_nvalid); + + ExecStoreVirtualTuple(insertslot); + } + + /* + * Constraints and GENERATED expressions might reference the tableoid + * column, so fill tts_tableOid with the desired value. (We must do + * this each time, because it gets overwritten with newrel's OID + * during storing.) + */ + insertslot->tts_tableOid = RelationGetRelid(pc->partRel); + + /* + * Now, evaluate any generated expressions whose inputs come from the + * new tuple. We assume these columns won't reference each other, so + * that there's no ordering dependency. + */ + evaluateGeneratedExpressionsAndCheckConstraints(pc->tab, pc->partRel, + insertslot, econtext); + + /* Write the tuple out to the new relation. */ + table_tuple_insert(pc->partRel, insertslot, mycid, + ti_options, pc->bistate); + + ResetExprContext(econtext); + } + + MemoryContextSwitchTo(oldCxt); + + table_endscan(scan); + UnregisterSnapshot(snapshot); + + if (tuple_map) + free_conversion_map(tuple_map); + + ExecDropSingleTupleTableSlot(srcslot); + + FreeExecutorState(estate); + + foreach_ptr(SplitPartitionContext, spc, partContexts) + deleteSplitPartitionContext(spc, wqueue, ti_options); + } + + /* + * ALTER TABLE SPLIT PARTITION INTO + */ + static void + ATExecSplitPartition(List **wqueue, AlteredTableInfo *tab, Relation rel, + PartitionCmd *cmd, AlterTableUtilityContext *context) + { + Relation splitRel; + Oid splitRelOid; + ListCell *listptr, + *listptr2; + bool isSameName = false; + char tmpRelName[NAMEDATALEN]; + List *newPartRels = NIL; + List *extDepState = NIL; + ObjectAddress object; + Oid defaultPartOid; + Oid save_userid; + int save_sec_context; + int save_nestlevel; + List *splitPartList; + + defaultPartOid = get_default_oid_from_partdesc(RelationGetPartitionDesc(rel, true)); + + /* + * Partition is already locked in the transformPartitionCmdForSplit + * function. + */ + splitRel = table_openrv(cmd->name, NoLock); + + splitRelOid = RelationGetRelid(splitRel); + + /* Check descriptions of new partitions. */ + foreach_node(SinglePartitionSpec, sps, cmd->partlist) + { + Oid existingRelid; + + /* Look up the existing relation by the new partition name. */ + RangeVarGetAndCheckCreationNamespace(sps->name, NoLock, &existingRelid); + + /* + * This would fail later on anyway if the relation already exists. But + * by catching it here, we can emit a nicer error message. + */ + if (existingRelid == splitRelOid && !isSameName) + /* One new partition can have the same name as a split partition. */ + isSameName = true; + else if (OidIsValid(existingRelid)) + ereport(ERROR, + errcode(ERRCODE_DUPLICATE_TABLE), + errmsg("relation \"%s\" already exists", sps->name->relname)); + } + + /* + * Collect extension dependencies from indexes on the split partition. We + * must do this before detaching it, so we can restore the dependencies on + * the new partitions' indexes later. + */ + splitPartList = list_make1_oid(splitRelOid); + + extDepState = collectPartitionIndexExtDeps(splitPartList); + list_free(splitPartList); + + /* Detach the split partition. */ + detachPartitionTable(rel, splitRel, defaultPartOid); + + /* + * Perform a preliminary check to determine whether it's safe to drop the + * split partition before we actually do so later. After merging rows into + * the new partitions via SplitPartitionMoveRows, all old partitions need + * to be dropped. However, since the drop behavior is DROP_RESTRICT and + * the merge process (SplitPartitionMoveRows) can be time-consuming, + * performing an early check on the drop eligibility of old partitions is + * preferable. + */ + object.objectId = splitRelOid; + object.classId = RelationRelationId; + object.objectSubId = 0; + performDeletionCheck(&object, DROP_RESTRICT, 0); + + /* + * If a new partition has the same name as the split partition, then we + * should rename the split partition to reuse its name. + */ + if (isSameName) + { + /* + * We must bump the command counter to make the split partition tuple + * visible for renaming. + */ + CommandCounterIncrement(); + /* Rename partition. */ + sprintf(tmpRelName, "split-%u-%X-tmp", RelationGetRelid(rel), MyProcPid); + RenameRelationInternal(splitRelOid, tmpRelName, true, false); + + /* + * We must bump the command counter to make the split partition tuple + * visible after renaming. + */ + CommandCounterIncrement(); + } + + /* Create new partitions (like a split partition), without indexes. */ + foreach_node(SinglePartitionSpec, sps, cmd->partlist) + { + Relation newPartRel; + + newPartRel = createPartitionTable(wqueue, sps->name, rel, + splitRel->rd_rel->relowner); + newPartRels = lappend(newPartRels, newPartRel); + } + + /* + * Switch to the table owner's userid, so that any index functions are run + * as that user. Also, lockdown security-restricted operations and + * arrange to make GUC variable changes local to this command. + * + * Need to do it after determining the namespace in the + * createPartitionTable() call. + */ + GetUserIdAndSecContext(&save_userid, &save_sec_context); + SetUserIdAndSecContext(splitRel->rd_rel->relowner, + save_sec_context | SECURITY_RESTRICTED_OPERATION); + save_nestlevel = NewGUCNestLevel(); + RestrictSearchPath(); + + /* Copy data from the split partition to the new partitions. */ + SplitPartitionMoveRows(wqueue, rel, splitRel, cmd->partlist, newPartRels); + /* Keep the lock until commit. */ + table_close(splitRel, NoLock); + + /* Attach new partitions to the partitioned table. */ + forboth(listptr, cmd->partlist, listptr2, newPartRels) + { + SinglePartitionSpec *sps = (SinglePartitionSpec *) lfirst(listptr); + Relation newPartRel = (Relation) lfirst(listptr2); + + /* + * wqueue = NULL: verification for each cloned constraint is not + * needed. + */ + attachPartitionTable(NULL, rel, newPartRel, sps->bound); + + /* + * Apply extension dependencies to the new partition's indexes. This + * preserves any "DEPENDS ON EXTENSION" settings from the split + * partition. + */ + applyPartitionIndexExtDeps(RelationGetRelid(newPartRel), extDepState); + + /* Keep the lock until commit. */ + table_close(newPartRel, NoLock); + } + + freePartitionIndexExtDeps(extDepState); + + /* Drop the split partition. */ + object.classId = RelationRelationId; + object.objectId = splitRelOid; + object.objectSubId = 0; + /* Probably DROP_CASCADE is not needed. */ + performDeletion(&object, DROP_RESTRICT, 0); + + /* Roll back any GUC changes executed by index functions. */ + AtEOXact_GUC(false, save_nestlevel); + + /* Restore the userid and security context. */ + SetUserIdAndSecContext(save_userid, save_sec_context); + } + + /* + * notnull_attnums: list of attribute numbers for all newly added NOT NULL + * constraints. + * + * For each NOT NULL attribute, first check whether the relation has a suitable + * index to fetch that attribute content. If so, using indexscan to verify that + * attribute contains no NULL values. + * + * Returning true means the new NOT NULL constraints are fully verified and no + * extra table scans are necessary. + */ + static bool + index_check_notnull(Relation relation, List *notnull_attnums) + { + SysScanDesc indscan; + ScanKeyData skey; + List *idxs = NIL; + List *attnums = NIL; + bool all_not_null = true; + ListCell *lc, + *lc2; + Relation pg_index; + Relation indexRel; + HeapTuple indexTuple; + + if (notnull_attnums == NIL) + return false; + + pg_index = table_open(IndexRelationId, AccessShareLock); + + /* Prepare to scan pg_index for entries having indrelid = this rel */ + ScanKeyInit(&skey, + Anum_pg_index_indrelid, + BTEqualStrategyNumber, + F_OIDEQ, + ObjectIdGetDatum(RelationGetRelid(relation))); + + indscan = systable_beginscan(pg_index, IndexIndrelidIndexId, true, + NULL, 1, &skey); + + while (HeapTupleIsValid(indexTuple = systable_getnext(indscan))) + { + Form_pg_attribute attr; + + Form_pg_index index = (Form_pg_index) GETSTRUCT(indexTuple); + + /* + * We only use non-deferred, valid, and live b-tree indexes to verify + * NOT NULL constraints. + */ + if (!index->indimmediate || !index->indisvalid || !index->indislive) + continue; + + /* cannot use expression index or partial index too */ + if (!heap_attisnull(indexTuple, Anum_pg_index_indexprs, NULL) || + !heap_attisnull(indexTuple, Anum_pg_index_indpred, NULL)) + continue; + + indexRel = index_open(index->indexrelid, AccessShareLock); + + if (indexRel->rd_rel->relam != BTREE_AM_OID) + { + index_close(indexRel, NoLock); + + continue; + } + + /* + * If the index is valid, but cannot yet be used, ignore it; See + * src/backend/access/heap/README.HOT for discussion. + */ + if (index->indcheckxmin && + !TransactionIdPrecedes(HeapTupleHeaderGetXmin(indexRel->rd_indextuple->t_data), + TransactionXmin)) + { + index_close(indexRel, NoLock); + continue; + } + + attr = TupleDescAttr(RelationGetDescr(relation), (index->indkey.values[0] - 1)); + + if (list_member_int(notnull_attnums, attr->attnum) && + !list_member_int(attnums, attr->attnum)) + { + attnums = lappend_int(attnums, attr->attnum); + idxs = lappend_oid(idxs, index->indexrelid); + } + + index_close(indexRel, NoLock); + } + systable_endscan(indscan); + table_close(pg_index, NoLock); + + /* + * Verify NOT NULL constraints using a suitable index, falling back to a + * full table scan if a suitable index is not present. + */ + if (attnums == NIL || + list_length(notnull_attnums) != list_length(attnums)) + return false; + + foreach_int(attno, notnull_attnums) + { + if (!list_member_int(attnums, attno)) + return false; + } + + forboth(lc, attnums, lc2, idxs) + { + SnapshotData DirtySnapshot; + IndexScanDesc indexScan; + ScanKeyData scankeys[INDEX_MAX_KEYS]; + AttrNumber sk_attno = -1; + AttrNumber attno = lfirst_int(lc); + Oid indexoid = lfirst_oid(lc2); + IndexInfo *indexInfo; + TupleTableSlot *existing_slot; + + indexRel = index_open(indexoid, NoLock); + indexInfo = BuildIndexInfo(indexRel); + existing_slot = table_slot_create(relation, NULL); + + /* + * Search the tuples that are in the index for any violations, + * including tuples that aren't visible yet. + */ + InitDirtySnapshot(DirtySnapshot); + + if (indexInfo->ii_IndexAttrNumbers[0] == attno) + sk_attno = 1; + else + elog(ERROR, "cache lookup failed for attribute number %d on index %u", + indexoid, attno); + + /* set up an IS NULL scan key so that we ignore not nulls */ + ScanKeyEntryInitialize(&scankeys[0], + SK_ISNULL | SK_SEARCHNULL, + sk_attno, /* index col to scan */ + InvalidStrategy, /* no strategy */ + InvalidOid, /* no strategy subtype */ + InvalidOid, /* no collation */ + InvalidOid, /* no reg proc for this */ + (Datum) 0); /* constant */ + + indexScan = index_beginscan(relation, + indexRel, + &DirtySnapshot, + NULL, + 1, + 0, + SO_NONE); + index_rescan(indexScan, scankeys, 1, NULL, 0); + + while (index_getnext_slot(indexScan, ForwardScanDirection, existing_slot)) + { + /* + * At this point, we have found that some attribute value is NULL. + * Since btree indexes are never lossy, no need recheck the NULL + * condition. + */ + all_not_null = false; + break; + } + + index_endscan(indexScan); + index_close(indexRel, NoLock); + ExecDropSingleTupleTableSlot(existing_slot); + + /* exit earlier */ + if (!all_not_null) + return false; + } + + return true; + } ++>>>>>>> theirs diff --cc src/test/modules/test_misc/meson.build index 5d81f5b13be,564b7d957ca..00000000000 --- a/src/test/modules/test_misc/meson.build +++ b/src/test/modules/test_misc/meson.build @@@ -23,7 -23,7 +23,11 @@@ tests += 't/012_ddlutils.pl', 't/013_temp_obj_multisession.pl', 't/014_log_statement_max_length.pl', ++<<<<<<< ours + 't/015_temp_schema_exit_deferrable.pl', ++======= + 't/015_indexscan_validate_notnull.pl', ++>>>>>>> theirs ], # The injection points are cluster-wide, so disable installcheck 'runningcheck': false,