agora inbox for [email protected]help / color / mirror / Atom feed
[PATCH v49 7/7] WIP add max_repack_replication_slots 290+ messages / 2 participants [nested] [flat]
* [PATCH v49 7/7] WIP add max_repack_replication_slots @ 2026-04-01 17:54 Álvaro Herrera <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Álvaro Herrera @ 2026-04-01 17:54 UTC (permalink / raw) --- src/backend/commands/repack_worker.c | 4 +- src/backend/replication/logical/launcher.c | 2 +- src/backend/replication/logical/slotsync.c | 5 +- src/backend/replication/slot.c | 77 +++++++++++-------- src/backend/replication/slotfuncs.c | 8 +- src/backend/replication/walsender.c | 4 +- src/backend/utils/misc/guc_parameters.dat | 8 ++ src/backend/utils/misc/postgresql.conf.sample | 2 + src/include/replication/slot.h | 6 +- 9 files changed, 74 insertions(+), 42 deletions(-) diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index 106b2b60441..00b21ede481 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -228,8 +228,8 @@ repack_setup_logical_decoding(Oid relid) * RS_TEMPORARY so that the slot gets cleaned up on ERROR. */ snprintf(NameStr(slotname), NAMEDATALEN, "repack_%d", MyProcPid); - ReplicationSlotCreate(NameStr(slotname), true, RS_TEMPORARY, false, false, - false); + ReplicationSlotCreate(NameStr(slotname), true, RS_TEMPORARY, false, true, + false, false); EnsureLogicalDecodingEnabled(); diff --git a/src/backend/replication/logical/launcher.c b/src/backend/replication/logical/launcher.c index 09964198550..d83125afd0d 100644 --- a/src/backend/replication/logical/launcher.c +++ b/src/backend/replication/logical/launcher.c @@ -1575,7 +1575,7 @@ CreateConflictDetectionSlot(void) errmsg("creating replication conflict detection slot")); ReplicationSlotCreate(CONFLICT_DETECTION_SLOT, false, RS_PERSISTENT, false, - false, false); + false, false, false); init_conflict_slot_xmin(); } diff --git a/src/backend/replication/logical/slotsync.c b/src/backend/replication/logical/slotsync.c index e75db69e3f6..1bc7f3f600d 100644 --- a/src/backend/replication/logical/slotsync.c +++ b/src/backend/replication/logical/slotsync.c @@ -425,7 +425,7 @@ get_local_synced_slots(void) LWLockAcquire(ReplicationSlotControlLock, LW_SHARED); - for (int i = 0; i < max_replication_slots; i++) + for (int i = 0; i < TotalMaxReplicationSlots; i++) { ReplicationSlot *s = &ReplicationSlotCtl->replication_slots[i]; @@ -814,6 +814,7 @@ synchronize_one_slot(RemoteSlot *remote_slot, Oid remote_dbid, */ ReplicationSlotCreate(remote_slot->name, true, RS_TEMPORARY, remote_slot->two_phase, + false, remote_slot->failover, true); @@ -1691,7 +1692,7 @@ update_synced_slots_inactive_since(void) LWLockAcquire(ReplicationSlotControlLock, LW_SHARED); - for (int i = 0; i < max_replication_slots; i++) + for (int i = 0; i < TotalMaxReplicationSlots; i++) { ReplicationSlot *s = &ReplicationSlotCtl->replication_slots[i]; diff --git a/src/backend/replication/slot.c b/src/backend/replication/slot.c index a9092fc2382..fe6bfeba25e 100644 --- a/src/backend/replication/slot.c +++ b/src/backend/replication/slot.c @@ -151,6 +151,9 @@ ReplicationSlot *MyReplicationSlot = NULL; /* GUC variables */ int max_replication_slots = 10; /* the maximum number of replication * slots */ +int max_repack_replication_slots = 1; /* the maximum number of slots + * for REPACK */ +int TotalMaxReplicationSlots = 0; /* sum of both */ /* * Invalidate replication slots that have remained idle longer than this @@ -190,12 +193,14 @@ ReplicationSlotsShmemSize(void) { Size size = 0; - if (max_replication_slots == 0) + TotalMaxReplicationSlots = max_replication_slots + max_repack_replication_slots; + + if (TotalMaxReplicationSlots == 0) return size; size = offsetof(ReplicationSlotCtlData, replication_slots); size = add_size(size, - mul_size(max_replication_slots, sizeof(ReplicationSlot))); + mul_size(TotalMaxReplicationSlots, sizeof(ReplicationSlot))); return size; } @@ -208,7 +213,7 @@ ReplicationSlotsShmemInit(void) { bool found; - if (max_replication_slots == 0) + if (TotalMaxReplicationSlots == 0) return; ReplicationSlotCtl = (ReplicationSlotCtlData *) @@ -222,7 +227,7 @@ ReplicationSlotsShmemInit(void) /* First time through, so initialize */ MemSet(ReplicationSlotCtl, 0, ReplicationSlotsShmemSize()); - for (i = 0; i < max_replication_slots; i++) + for (i = 0; i < TotalMaxReplicationSlots; i++) { ReplicationSlot *slot = &ReplicationSlotCtl->replication_slots[i]; @@ -372,6 +377,7 @@ IsSlotForConflictCheck(const char *name) * db_specific: logical decoding is db specific; if the slot is going to * be used for that pass true, otherwise false. * two_phase: If enabled, allows decoding of prepared transactions. + * repack: If true, use a slot from the pool for REPACK. * failover: If enabled, allows the slot to be synced to standbys so * that logical replication can be resumed after failover. * synced: True if the slot is synchronized from the primary server. @@ -379,10 +385,11 @@ IsSlotForConflictCheck(const char *name) void ReplicationSlotCreate(const char *name, bool db_specific, ReplicationSlotPersistency persistency, - bool two_phase, bool failover, bool synced) + bool two_phase, bool repack, bool failover, bool synced) { ReplicationSlot *slot = NULL; - int i; + int startpoint, + endpoint; Assert(MyReplicationSlot == NULL); @@ -431,12 +438,16 @@ ReplicationSlotCreate(const char *name, bool db_specific, LWLockAcquire(ReplicationSlotAllocationLock, LW_EXCLUSIVE); /* - * Check for name collision, and identify an allocatable slot. We need to - * hold ReplicationSlotControlLock in shared mode for this, so that nobody - * else can change the in_use flags while we're looking at them. + * Check for name collision (across the whole array), and identify an + * allocatable slot (in the array slice specific to our current use case: + * either general, or REPACK only). We need to hold + * ReplicationSlotControlLock in shared mode for this, so that nobody else + * can change the in_use flags while we're looking at them. */ LWLockAcquire(ReplicationSlotControlLock, LW_SHARED); - for (i = 0; i < max_replication_slots; i++) + startpoint = repack ? max_replication_slots : 0; + endpoint = repack ? TotalMaxReplicationSlots : max_replication_slots; + for (int i = 0; i < TotalMaxReplicationSlots; i++) { ReplicationSlot *s = &ReplicationSlotCtl->replication_slots[i]; @@ -444,7 +455,9 @@ ReplicationSlotCreate(const char *name, bool db_specific, ereport(ERROR, (errcode(ERRCODE_DUPLICATE_OBJECT), errmsg("replication slot \"%s\" already exists", name))); - if (!s->in_use && slot == NULL) + + if (i >= startpoint && i < endpoint && + !s->in_use && slot == NULL) slot = s; } LWLockRelease(ReplicationSlotControlLock); @@ -547,7 +560,7 @@ SearchNamedReplicationSlot(const char *name, bool need_lock) if (need_lock) LWLockAcquire(ReplicationSlotControlLock, LW_SHARED); - for (i = 0; i < max_replication_slots; i++) + for (i = 0; i < TotalMaxReplicationSlots; i++) { ReplicationSlot *s = &ReplicationSlotCtl->replication_slots[i]; @@ -575,7 +588,7 @@ int ReplicationSlotIndex(ReplicationSlot *slot) { Assert(slot >= ReplicationSlotCtl->replication_slots && - slot < ReplicationSlotCtl->replication_slots + max_replication_slots); + slot < ReplicationSlotCtl->replication_slots + TotalMaxReplicationSlots); return slot - ReplicationSlotCtl->replication_slots; } @@ -869,7 +882,7 @@ ReplicationSlotCleanup(bool synced_only) restart: found_valid_logicalslot = false; LWLockAcquire(ReplicationSlotControlLock, LW_SHARED); - for (i = 0; i < max_replication_slots; i++) + for (i = 0; i < TotalMaxReplicationSlots; i++) { ReplicationSlot *s = &ReplicationSlotCtl->replication_slots[i]; @@ -1251,7 +1264,7 @@ ReplicationSlotsComputeRequiredXmin(bool already_locked) if (!already_locked) LWLockAcquire(ReplicationSlotControlLock, LW_SHARED); - for (i = 0; i < max_replication_slots; i++) + for (i = 0; i < TotalMaxReplicationSlots; i++) { ReplicationSlot *s = &ReplicationSlotCtl->replication_slots[i]; TransactionId effective_xmin; @@ -1306,7 +1319,7 @@ ReplicationSlotsComputeRequiredLSN(void) Assert(ReplicationSlotCtl != NULL); LWLockAcquire(ReplicationSlotControlLock, LW_SHARED); - for (i = 0; i < max_replication_slots; i++) + for (i = 0; i < TotalMaxReplicationSlots; i++) { ReplicationSlot *s = &ReplicationSlotCtl->replication_slots[i]; XLogRecPtr restart_lsn; @@ -1373,12 +1386,12 @@ ReplicationSlotsComputeLogicalRestartLSN(void) XLogRecPtr result = InvalidXLogRecPtr; int i; - if (max_replication_slots <= 0) + if (TotalMaxReplicationSlots <= 0) return InvalidXLogRecPtr; LWLockAcquire(ReplicationSlotControlLock, LW_SHARED); - for (i = 0; i < max_replication_slots; i++) + for (i = 0; i < TotalMaxReplicationSlots; i++) { ReplicationSlot *s; XLogRecPtr restart_lsn; @@ -1453,11 +1466,11 @@ ReplicationSlotsCountDBSlots(Oid dboid, int *nslots, int *nactive) *nslots = *nactive = 0; - if (max_replication_slots <= 0) + if (TotalMaxReplicationSlots <= 0) return false; LWLockAcquire(ReplicationSlotControlLock, LW_SHARED); - for (i = 0; i < max_replication_slots; i++) + for (i = 0; i < TotalMaxReplicationSlots; i++) { ReplicationSlot *s; @@ -1514,13 +1527,13 @@ ReplicationSlotsDropDBSlots(Oid dboid) bool found_valid_logicalslot; bool dropped = false; - if (max_replication_slots <= 0) + if (TotalMaxReplicationSlots <= 0) return; restart: found_valid_logicalslot = false; LWLockAcquire(ReplicationSlotControlLock, LW_SHARED); - for (i = 0; i < max_replication_slots; i++) + for (i = 0; i < TotalMaxReplicationSlots; i++) { ReplicationSlot *s; char *slotname; @@ -1617,11 +1630,11 @@ CheckLogicalSlotExists(void) { bool found = false; - if (max_replication_slots <= 0) + if (TotalMaxReplicationSlots <= 0) return false; LWLockAcquire(ReplicationSlotControlLock, LW_SHARED); - for (int i = 0; i < max_replication_slots; i++) + for (int i = 0; i < TotalMaxReplicationSlots; i++) { ReplicationSlot *s; bool invalidated; @@ -1662,7 +1675,10 @@ CheckSlotRequirements(void) * needs the same check. */ - if (max_replication_slots == 0) + /* FIXME how to relax this for repack in a way that doesn't mess everything + * up? + */ + if (TotalMaxReplicationSlots == 0) ereport(ERROR, (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), errmsg("replication slots can only be used if \"max_replication_slots\" > 0"))); @@ -2216,7 +2232,7 @@ InvalidateObsoleteReplicationSlots(uint32 possible_causes, Assert(!(possible_causes & RS_INVAL_WAL_REMOVED) || oldestSegno > 0); Assert(possible_causes != RS_INVAL_NONE); - if (max_replication_slots == 0) + if (TotalMaxReplicationSlots == 0) return invalidated; XLogSegNoOffsetToRecPtr(oldestSegno, 0, wal_segment_size, oldestLSN); @@ -2224,7 +2240,7 @@ InvalidateObsoleteReplicationSlots(uint32 possible_causes, restart: found_valid_logicalslot = false; LWLockAcquire(ReplicationSlotControlLock, LW_SHARED); - for (int i = 0; i < max_replication_slots; i++) + for (int i = 0; i < TotalMaxReplicationSlots; i++) { ReplicationSlot *s = &ReplicationSlotCtl->replication_slots[i]; bool released_lock = false; @@ -2329,7 +2345,7 @@ CheckPointReplicationSlots(bool is_shutdown) */ LWLockAcquire(ReplicationSlotAllocationLock, LW_SHARED); - for (i = 0; i < max_replication_slots; i++) + for (i = 0; i < TotalMaxReplicationSlots; i++) { ReplicationSlot *s = &ReplicationSlotCtl->replication_slots[i]; char path[MAXPGPATH]; @@ -2430,7 +2446,7 @@ StartupReplicationSlots(void) FreeDir(replication_dir); /* currently no slots exist, we're done. */ - if (max_replication_slots <= 0) + if (TotalMaxReplicationSlots <= 0) return; /* Now that we have recovered all the data, compute replication xmin */ @@ -2860,7 +2876,7 @@ RestoreSlotFromDisk(const char *name) errhint("Change \"wal_level\" to be \"replica\" or higher."))); /* nothing can be active yet, don't lock anything */ - for (i = 0; i < max_replication_slots; i++) + for (i = 0; i < TotalMaxReplicationSlots; i++) { ReplicationSlot *slot; @@ -2902,6 +2918,7 @@ RestoreSlotFromDisk(const char *name) break; } + /* XXX might be misleading if the slots previously in use were REPACK. */ if (!restored) ereport(FATAL, (errmsg("too many replication slots active before shutdown"), diff --git a/src/backend/replication/slotfuncs.c b/src/backend/replication/slotfuncs.c index 9f5e4f998fe..cdb4dbd87c9 100644 --- a/src/backend/replication/slotfuncs.c +++ b/src/backend/replication/slotfuncs.c @@ -53,7 +53,7 @@ create_physical_replication_slot(char *name, bool immediately_reserve, /* acquire replication slot, this will check for conflicting names */ ReplicationSlotCreate(name, false, temporary ? RS_TEMPORARY : RS_PERSISTENT, false, - false, false); + false, false, false); if (immediately_reserve) { @@ -146,7 +146,7 @@ create_logical_replication_slot(char *name, char *plugin, */ ReplicationSlotCreate(name, true, temporary ? RS_TEMPORARY : RS_EPHEMERAL, two_phase, - failover, false); + false, failover, false); /* * Ensure the logical decoding is enabled before initializing the logical @@ -270,7 +270,7 @@ pg_get_replication_slots(PG_FUNCTION_ARGS) currlsn = GetXLogWriteRecPtr(); LWLockAcquire(ReplicationSlotControlLock, LW_SHARED); - for (slotno = 0; slotno < max_replication_slots; slotno++) + for (slotno = 0; slotno < TotalMaxReplicationSlots; slotno++) { ReplicationSlot *slot = &ReplicationSlotCtl->replication_slots[slotno]; ReplicationSlot slot_contents; @@ -665,7 +665,7 @@ copy_replication_slot(FunctionCallInfo fcinfo, bool logical_slot) * managed to create the new slot, we advance the new slot's restart_lsn * to the source slot's updated restart_lsn the second time we lock it. */ - for (int i = 0; i < max_replication_slots; i++) + for (int i = 0; i < TotalMaxReplicationSlots; i++) { ReplicationSlot *s = &ReplicationSlotCtl->replication_slots[i]; diff --git a/src/backend/replication/walsender.c b/src/backend/replication/walsender.c index fbbe09135bf..caea3ed1257 100644 --- a/src/backend/replication/walsender.c +++ b/src/backend/replication/walsender.c @@ -1220,7 +1220,7 @@ CreateReplicationSlot(CreateReplicationSlotCmd *cmd) { ReplicationSlotCreate(cmd->slotname, false, cmd->temporary ? RS_TEMPORARY : RS_PERSISTENT, - false, false, false); + false, false, false, false); if (reserve_wal) { @@ -1251,7 +1251,7 @@ CreateReplicationSlot(CreateReplicationSlotCmd *cmd) */ ReplicationSlotCreate(cmd->slotname, true, cmd->temporary ? RS_TEMPORARY : RS_EPHEMERAL, - two_phase, failover, false); + two_phase, false, failover, false); /* * Do options check early so that we can bail before calling the diff --git a/src/backend/utils/misc/guc_parameters.dat b/src/backend/utils/misc/guc_parameters.dat index 0a862693fcd..d12a3bd7bdf 100644 --- a/src/backend/utils/misc/guc_parameters.dat +++ b/src/backend/utils/misc/guc_parameters.dat @@ -2070,6 +2070,14 @@ max => 'MAX_BACKENDS', }, +{ name => 'max_repack_replication_slots', type => 'int', context => 'PGC_POSTMASTER', group => 'REPLICATION_SENDING', + short_desc => 'Sets the maximum number of replication slots for use by REPACK.', + variable => 'max_repack_replication_slots', + boot_val => '1', + min => '0', + max => 'MAX_BACKENDS /* XXX? */', +}, + /* see max_wal_senders */ { name => 'max_replication_slots', type => 'int', context => 'PGC_POSTMASTER', group => 'REPLICATION_SENDING', short_desc => 'Sets the maximum number of simultaneously defined replication slots.', diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample index cf15597385b..15014f37753 100644 --- a/src/backend/utils/misc/postgresql.conf.sample +++ b/src/backend/utils/misc/postgresql.conf.sample @@ -348,6 +348,8 @@ # (change requires restart) #max_replication_slots = 10 # max number of replication slots # (change requires restart) +#max_repack_replication_slots = 1 # max number of replication slots for REPACK + # (change requires restart) #wal_keep_size = 0 # in megabytes; 0 disables #max_slot_wal_keep_size = -1 # in megabytes; -1 disables #idle_replication_slot_timeout = 0 # in seconds; 0 disables diff --git a/src/include/replication/slot.h b/src/include/replication/slot.h index 4b4709f6e2c..88953576894 100644 --- a/src/include/replication/slot.h +++ b/src/include/replication/slot.h @@ -324,9 +324,13 @@ extern PGDLLIMPORT ReplicationSlot *MyReplicationSlot; /* GUCs */ extern PGDLLIMPORT int max_replication_slots; +extern PGDLLIMPORT int max_repack_replication_slots; extern PGDLLIMPORT char *synchronized_standby_slots; extern PGDLLIMPORT int idle_replication_slot_timeout_secs; +/* only for slotfuncs.c, slotsync.c etc */ +extern int TotalMaxReplicationSlots; + /* shmem initialization functions */ extern Size ReplicationSlotsShmemSize(void); extern void ReplicationSlotsShmemInit(void); @@ -334,7 +338,7 @@ extern void ReplicationSlotsShmemInit(void); /* management of individual slots */ extern void ReplicationSlotCreate(const char *name, bool db_specific, ReplicationSlotPersistency persistency, - bool two_phase, bool failover, + bool two_phase, bool repack, bool failover, bool synced); extern void ReplicationSlotPersist(void); extern void ReplicationSlotDrop(const char *name, bool nowait); -- 2.47.3 --3n3vqr5y2jhknrqj-- ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-06-16 11:54 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index 2268cc277bc..127d4415084 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -609,11 +608,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -627,11 +623,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -705,6 +696,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -881,11 +874,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -901,8 +892,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -922,6 +912,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -932,33 +924,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2346,21 +2341,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2372,6 +2391,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2380,76 +2402,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v01-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-06-16 11:54 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index 2268cc277bc..127d4415084 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -609,11 +608,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -627,11 +623,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -705,6 +696,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -881,11 +874,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -901,8 +892,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -922,6 +912,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -932,33 +924,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2346,21 +2341,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2372,6 +2391,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2380,76 +2402,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v01-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-06-16 11:54 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index 2268cc277bc..127d4415084 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -609,11 +608,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -627,11 +623,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -705,6 +696,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -881,11 +874,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -901,8 +892,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -922,6 +912,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -932,33 +924,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2346,21 +2341,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2372,6 +2391,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2380,76 +2402,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v01-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-06-16 11:54 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index 2268cc277bc..127d4415084 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -609,11 +608,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -627,11 +623,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -705,6 +696,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -881,11 +874,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -901,8 +892,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -922,6 +912,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -932,33 +924,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2346,21 +2341,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2372,6 +2391,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2380,76 +2402,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v01-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-06-16 11:54 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index 2268cc277bc..127d4415084 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -609,11 +608,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -627,11 +623,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -705,6 +696,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -881,11 +874,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -901,8 +892,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -922,6 +912,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -932,33 +924,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2346,21 +2341,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2372,6 +2391,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2380,76 +2402,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v01-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-06-16 11:54 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index 2268cc277bc..127d4415084 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -609,11 +608,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -627,11 +623,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -705,6 +696,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -881,11 +874,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -901,8 +892,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -922,6 +912,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -932,33 +924,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2346,21 +2341,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2372,6 +2391,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2380,76 +2402,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v01-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-06-16 11:54 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index 2268cc277bc..127d4415084 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -609,11 +608,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -627,11 +623,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -705,6 +696,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -881,11 +874,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -901,8 +892,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -922,6 +912,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -932,33 +924,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2346,21 +2341,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2372,6 +2391,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2380,76 +2402,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v01-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-06-16 11:54 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index 2268cc277bc..127d4415084 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -609,11 +608,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -627,11 +623,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -705,6 +696,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -881,11 +874,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -901,8 +892,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -922,6 +912,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -932,33 +924,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2346,21 +2341,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2372,6 +2391,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2380,76 +2402,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v01-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-06-16 11:54 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index 2268cc277bc..127d4415084 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -609,11 +608,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -627,11 +623,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -705,6 +696,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -881,11 +874,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -901,8 +892,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -922,6 +912,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -932,33 +924,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2346,21 +2341,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2372,6 +2391,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2380,76 +2402,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v01-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-06-16 11:54 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index 2268cc277bc..127d4415084 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -609,11 +608,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -627,11 +623,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -705,6 +696,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -881,11 +874,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -901,8 +892,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -922,6 +912,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -932,33 +924,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2346,21 +2341,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2372,6 +2391,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2380,76 +2402,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v01-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-06-16 11:54 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index 2268cc277bc..127d4415084 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -609,11 +608,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -627,11 +623,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -705,6 +696,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -881,11 +874,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -901,8 +892,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -922,6 +912,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -932,33 +924,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2346,21 +2341,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2372,6 +2391,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2380,76 +2402,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v01-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-06-16 11:54 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index 2268cc277bc..127d4415084 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -609,11 +608,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -627,11 +623,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -705,6 +696,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -881,11 +874,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -901,8 +892,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -922,6 +912,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -932,33 +924,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2346,21 +2341,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2372,6 +2391,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2380,76 +2402,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v01-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-06-16 11:54 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index 2268cc277bc..127d4415084 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -609,11 +608,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -627,11 +623,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -705,6 +696,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -881,11 +874,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -901,8 +892,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -922,6 +912,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -932,33 +924,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2346,21 +2341,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2372,6 +2391,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2380,76 +2402,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v01-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-06-16 11:54 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index 2268cc277bc..127d4415084 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -609,11 +608,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -627,11 +623,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -705,6 +696,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -881,11 +874,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -901,8 +892,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -922,6 +912,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -932,33 +924,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2346,21 +2341,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2372,6 +2391,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2380,76 +2402,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v01-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-06-16 11:54 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index 2268cc277bc..127d4415084 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -609,11 +608,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -627,11 +623,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -705,6 +696,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -881,11 +874,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -901,8 +892,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -922,6 +912,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -932,33 +924,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2346,21 +2341,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2372,6 +2391,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2380,76 +2402,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v01-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-06-16 11:54 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index 2268cc277bc..127d4415084 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -609,11 +608,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -627,11 +623,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -705,6 +696,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -881,11 +874,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -901,8 +892,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -922,6 +912,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -932,33 +924,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2346,21 +2341,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2372,6 +2391,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2380,76 +2402,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v01-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-06-16 11:54 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index 2268cc277bc..127d4415084 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -609,11 +608,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -627,11 +623,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -705,6 +696,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -881,11 +874,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -901,8 +892,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -922,6 +912,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -932,33 +924,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2346,21 +2341,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2372,6 +2391,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2380,76 +2402,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v01-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-06-16 11:54 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index 2268cc277bc..127d4415084 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -609,11 +608,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -627,11 +623,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -705,6 +696,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -881,11 +874,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -901,8 +892,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -922,6 +912,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -932,33 +924,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2346,21 +2341,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2372,6 +2391,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2380,76 +2402,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v01-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-06-16 11:54 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index 2268cc277bc..127d4415084 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -609,11 +608,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -627,11 +623,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -705,6 +696,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -881,11 +874,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -901,8 +892,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -922,6 +912,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -932,33 +924,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2346,21 +2341,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2372,6 +2391,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2380,76 +2402,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v01-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-06-16 11:54 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index 2268cc277bc..127d4415084 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -609,11 +608,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -627,11 +623,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -705,6 +696,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -881,11 +874,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -901,8 +892,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -922,6 +912,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -932,33 +924,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2346,21 +2341,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2372,6 +2391,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2380,76 +2402,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v01-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-06-16 11:54 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index 2268cc277bc..127d4415084 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -609,11 +608,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -627,11 +623,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -705,6 +696,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -881,11 +874,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -901,8 +892,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -922,6 +912,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -932,33 +924,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2346,21 +2341,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2372,6 +2391,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2380,76 +2402,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v01-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-06-16 11:54 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index 2268cc277bc..127d4415084 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -609,11 +608,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -627,11 +623,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -705,6 +696,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -881,11 +874,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -901,8 +892,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -922,6 +912,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -932,33 +924,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2346,21 +2341,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2372,6 +2391,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2380,76 +2402,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v01-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-06-16 11:54 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index 2268cc277bc..127d4415084 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -609,11 +608,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -627,11 +623,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -705,6 +696,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -881,11 +874,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -901,8 +892,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -922,6 +912,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -932,33 +924,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2346,21 +2341,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2372,6 +2391,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2380,76 +2402,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v01-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-06-16 11:54 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index 2268cc277bc..127d4415084 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -609,11 +608,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -627,11 +623,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -705,6 +696,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -881,11 +874,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -901,8 +892,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -922,6 +912,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -932,33 +924,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2346,21 +2341,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2372,6 +2391,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2380,76 +2402,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v01-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-06-16 11:54 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index 2268cc277bc..127d4415084 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -609,11 +608,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -627,11 +623,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -705,6 +696,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -881,11 +874,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -901,8 +892,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -922,6 +912,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -932,33 +924,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2346,21 +2341,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2372,6 +2391,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2380,76 +2402,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v01-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-06-16 11:54 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index 2268cc277bc..127d4415084 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -609,11 +608,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -627,11 +623,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -705,6 +696,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -881,11 +874,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -901,8 +892,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -922,6 +912,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -932,33 +924,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2346,21 +2341,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2372,6 +2391,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2380,76 +2402,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v01-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-06-16 11:54 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index 2268cc277bc..127d4415084 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -609,11 +608,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -627,11 +623,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -705,6 +696,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -881,11 +874,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -901,8 +892,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -922,6 +912,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -932,33 +924,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2346,21 +2341,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2372,6 +2391,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2380,76 +2402,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v01-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-06-16 11:54 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index 2268cc277bc..127d4415084 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -609,11 +608,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -627,11 +623,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -705,6 +696,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -881,11 +874,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -901,8 +892,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -922,6 +912,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -932,33 +924,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2346,21 +2341,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2372,6 +2391,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2380,76 +2402,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v01-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-06-16 11:54 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index 2268cc277bc..127d4415084 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -609,11 +608,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -627,11 +623,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -705,6 +696,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -881,11 +874,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -901,8 +892,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -922,6 +912,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -932,33 +924,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2346,21 +2341,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2372,6 +2391,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2380,76 +2402,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v01-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-06-16 11:54 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index 2268cc277bc..127d4415084 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -609,11 +608,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -627,11 +623,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -705,6 +696,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -881,11 +874,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -901,8 +892,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -922,6 +912,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -932,33 +924,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2346,21 +2341,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2372,6 +2391,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2380,76 +2402,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v01-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-06-16 11:54 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index 2268cc277bc..127d4415084 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -609,11 +608,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -627,11 +623,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -705,6 +696,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -881,11 +874,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -901,8 +892,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -922,6 +912,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -932,33 +924,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2346,21 +2341,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2372,6 +2391,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2380,76 +2402,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v01-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-06-16 11:54 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index 2268cc277bc..127d4415084 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -609,11 +608,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -627,11 +623,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -705,6 +696,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -881,11 +874,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -901,8 +892,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -922,6 +912,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -932,33 +924,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2346,21 +2341,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2372,6 +2391,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2380,76 +2402,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v01-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-06-16 11:54 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index 2268cc277bc..127d4415084 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -609,11 +608,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -627,11 +623,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -705,6 +696,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -881,11 +874,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -901,8 +892,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -922,6 +912,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -932,33 +924,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2346,21 +2341,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2372,6 +2391,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2380,76 +2402,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v01-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-06-16 11:54 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index 2268cc277bc..127d4415084 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -609,11 +608,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -627,11 +623,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -705,6 +696,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -881,11 +874,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -901,8 +892,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -922,6 +912,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -932,33 +924,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2346,21 +2341,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2372,6 +2391,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2380,76 +2402,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v01-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-06-16 11:54 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index 2268cc277bc..127d4415084 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -609,11 +608,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -627,11 +623,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -705,6 +696,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -881,11 +874,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -901,8 +892,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -922,6 +912,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -932,33 +924,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2346,21 +2341,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2372,6 +2391,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2380,76 +2402,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v01-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-06-16 11:54 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index 2268cc277bc..127d4415084 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -609,11 +608,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -627,11 +623,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -705,6 +696,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -881,11 +874,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -901,8 +892,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -922,6 +912,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -932,33 +924,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2346,21 +2341,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2372,6 +2391,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2380,76 +2402,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v01-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-06-16 11:54 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index 2268cc277bc..127d4415084 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -609,11 +608,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -627,11 +623,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -705,6 +696,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -881,11 +874,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -901,8 +892,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -922,6 +912,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -932,33 +924,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2346,21 +2341,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2372,6 +2391,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2380,76 +2402,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v01-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-06-16 11:54 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index 2268cc277bc..127d4415084 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -609,11 +608,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -627,11 +623,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -705,6 +696,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -881,11 +874,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -901,8 +892,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -922,6 +912,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -932,33 +924,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2346,21 +2341,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2372,6 +2391,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2380,76 +2402,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v01-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-06-16 11:54 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index 2268cc277bc..127d4415084 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -609,11 +608,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -627,11 +623,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -705,6 +696,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -881,11 +874,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -901,8 +892,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -922,6 +912,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -932,33 +924,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2346,21 +2341,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2372,6 +2391,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2380,76 +2402,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v01-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-06-16 11:54 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index 2268cc277bc..127d4415084 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -609,11 +608,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -627,11 +623,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -705,6 +696,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -881,11 +874,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -901,8 +892,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -922,6 +912,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -932,33 +924,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2346,21 +2341,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2372,6 +2391,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2380,76 +2402,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v01-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-06-16 11:54 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index 2268cc277bc..127d4415084 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -609,11 +608,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -627,11 +623,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -705,6 +696,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -881,11 +874,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -901,8 +892,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -922,6 +912,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -932,33 +924,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2346,21 +2341,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2372,6 +2391,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2380,76 +2402,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v01-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-06-16 11:54 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index 2268cc277bc..127d4415084 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -609,11 +608,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -627,11 +623,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -705,6 +696,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -881,11 +874,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -901,8 +892,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -922,6 +912,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -932,33 +924,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2346,21 +2341,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2372,6 +2391,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2380,76 +2402,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v01-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-06-16 11:54 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index 2268cc277bc..127d4415084 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -609,11 +608,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -627,11 +623,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -705,6 +696,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -881,11 +874,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -901,8 +892,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -922,6 +912,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -932,33 +924,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2346,21 +2341,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2372,6 +2391,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2380,76 +2402,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v01-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-06-16 11:54 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index 2268cc277bc..127d4415084 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -609,11 +608,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -627,11 +623,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -705,6 +696,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -881,11 +874,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -901,8 +892,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -922,6 +912,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -932,33 +924,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2346,21 +2341,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2372,6 +2391,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2380,76 +2402,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v01-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-06-16 11:54 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index 2268cc277bc..127d4415084 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -609,11 +608,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -627,11 +623,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -705,6 +696,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -881,11 +874,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -901,8 +892,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -922,6 +912,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -932,33 +924,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2346,21 +2341,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2372,6 +2391,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2380,76 +2402,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v01-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-06-16 11:54 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index 2268cc277bc..127d4415084 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -609,11 +608,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -627,11 +623,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -705,6 +696,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -881,11 +874,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -901,8 +892,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -922,6 +912,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -932,33 +924,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2346,21 +2341,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2372,6 +2391,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2380,76 +2402,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v01-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-06-16 11:54 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index 2268cc277bc..127d4415084 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -609,11 +608,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -627,11 +623,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -705,6 +696,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -881,11 +874,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -901,8 +892,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -922,6 +912,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -932,33 +924,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2346,21 +2341,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2372,6 +2391,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2380,76 +2402,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v01-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-06-16 11:54 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index 2268cc277bc..127d4415084 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -609,11 +608,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -627,11 +623,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -705,6 +696,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -881,11 +874,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -901,8 +892,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -922,6 +912,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -932,33 +924,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2346,21 +2341,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2372,6 +2391,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2380,76 +2402,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v01-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-06-16 11:54 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index 2268cc277bc..127d4415084 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -609,11 +608,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -627,11 +623,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -705,6 +696,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -881,11 +874,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -901,8 +892,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -922,6 +912,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -932,33 +924,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2346,21 +2341,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2372,6 +2391,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2380,76 +2402,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v01-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-06-16 11:54 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index 2268cc277bc..127d4415084 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -609,11 +608,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -627,11 +623,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -705,6 +696,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -881,11 +874,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -901,8 +892,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -922,6 +912,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -932,33 +924,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2346,21 +2341,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2372,6 +2391,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2380,76 +2402,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v01-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-06-16 11:54 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index 2268cc277bc..127d4415084 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -609,11 +608,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -627,11 +623,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -705,6 +696,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -881,11 +874,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -901,8 +892,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -922,6 +912,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -932,33 +924,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2346,21 +2341,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2372,6 +2391,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2380,76 +2402,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v01-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-06-16 11:54 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index 2268cc277bc..127d4415084 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -609,11 +608,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -627,11 +623,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -705,6 +696,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -881,11 +874,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -901,8 +892,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -922,6 +912,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -932,33 +924,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2346,21 +2341,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2372,6 +2391,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2380,76 +2402,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v01-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-06-16 11:54 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index 2268cc277bc..127d4415084 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -609,11 +608,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -627,11 +623,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -705,6 +696,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -881,11 +874,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -901,8 +892,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -922,6 +912,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -932,33 +924,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2346,21 +2341,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2372,6 +2391,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2380,76 +2402,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v01-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-06-16 11:54 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index 2268cc277bc..127d4415084 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -609,11 +608,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -627,11 +623,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -705,6 +696,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -881,11 +874,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -901,8 +892,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -922,6 +912,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -932,33 +924,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2346,21 +2341,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2372,6 +2391,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2380,76 +2402,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v01-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-06-16 11:54 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index 2268cc277bc..127d4415084 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -609,11 +608,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -627,11 +623,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -705,6 +696,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -881,11 +874,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -901,8 +892,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -922,6 +912,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -932,33 +924,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2346,21 +2341,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2372,6 +2391,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2380,76 +2402,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v01-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-06-16 11:54 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index 2268cc277bc..127d4415084 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -609,11 +608,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -627,11 +623,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -705,6 +696,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -881,11 +874,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -901,8 +892,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -922,6 +912,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -932,33 +924,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2346,21 +2341,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2372,6 +2391,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2380,76 +2402,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v01-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-06-16 11:54 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index 2268cc277bc..127d4415084 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -609,11 +608,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -627,11 +623,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -705,6 +696,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -881,11 +874,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -901,8 +892,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -922,6 +912,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -932,33 +924,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2346,21 +2341,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2372,6 +2391,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2380,76 +2402,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v01-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-06-16 11:54 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index 2268cc277bc..127d4415084 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -609,11 +608,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -627,11 +623,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -705,6 +696,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -881,11 +874,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -901,8 +892,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -922,6 +912,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -932,33 +924,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2346,21 +2341,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2372,6 +2391,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2380,76 +2402,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v01-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-06-16 11:54 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index 2268cc277bc..127d4415084 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -609,11 +608,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -627,11 +623,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -705,6 +696,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -881,11 +874,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -901,8 +892,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -922,6 +912,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -932,33 +924,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2346,21 +2341,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2372,6 +2391,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2380,76 +2402,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v01-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-06-16 11:54 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index 2268cc277bc..127d4415084 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -609,11 +608,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -627,11 +623,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -705,6 +696,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -881,11 +874,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -901,8 +892,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -922,6 +912,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -932,33 +924,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2346,21 +2341,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2372,6 +2391,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2380,76 +2402,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v01-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-06-16 11:54 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index 2268cc277bc..127d4415084 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -609,11 +608,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -627,11 +623,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -705,6 +696,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -881,11 +874,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -901,8 +892,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -922,6 +912,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -932,33 +924,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2346,21 +2341,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2372,6 +2391,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2380,76 +2402,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v01-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-06-16 11:54 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index 2268cc277bc..127d4415084 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -609,11 +608,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -627,11 +623,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -705,6 +696,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -881,11 +874,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -901,8 +892,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -922,6 +912,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -932,33 +924,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2346,21 +2341,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2372,6 +2391,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2380,76 +2402,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v01-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-06-16 11:54 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index 2268cc277bc..127d4415084 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -609,11 +608,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -627,11 +623,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -705,6 +696,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -881,11 +874,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -901,8 +892,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -922,6 +912,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -932,33 +924,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2346,21 +2341,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2372,6 +2391,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2380,76 +2402,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v01-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-06-16 11:54 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index 2268cc277bc..127d4415084 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -609,11 +608,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -627,11 +623,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -705,6 +696,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -881,11 +874,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -901,8 +892,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -922,6 +912,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -932,33 +924,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2346,21 +2341,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2372,6 +2391,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2380,76 +2402,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v01-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-06-16 11:54 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index 2268cc277bc..127d4415084 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -609,11 +608,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -627,11 +623,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -705,6 +696,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -881,11 +874,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -901,8 +892,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -922,6 +912,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -932,33 +924,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2346,21 +2341,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2372,6 +2391,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2380,76 +2402,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v01-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-06-16 11:54 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index 2268cc277bc..127d4415084 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -609,11 +608,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -627,11 +623,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -705,6 +696,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -881,11 +874,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -901,8 +892,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -922,6 +912,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -932,33 +924,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2346,21 +2341,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2372,6 +2391,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2380,76 +2402,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v01-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-06-16 11:54 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index 2268cc277bc..127d4415084 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -609,11 +608,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -627,11 +623,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -705,6 +696,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -881,11 +874,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -901,8 +892,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -922,6 +912,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -932,33 +924,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2346,21 +2341,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2372,6 +2391,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2380,76 +2402,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v01-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-06-16 11:54 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index 2268cc277bc..127d4415084 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -609,11 +608,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -627,11 +623,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -705,6 +696,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -881,11 +874,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -901,8 +892,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -922,6 +912,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -932,33 +924,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2346,21 +2341,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2372,6 +2391,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2380,76 +2402,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v01-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-06-16 11:54 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index 2268cc277bc..127d4415084 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -609,11 +608,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -627,11 +623,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -705,6 +696,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -881,11 +874,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -901,8 +892,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -922,6 +912,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -932,33 +924,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2346,21 +2341,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2372,6 +2391,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2380,76 +2402,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v01-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-06-16 11:54 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index 2268cc277bc..127d4415084 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -609,11 +608,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -627,11 +623,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -705,6 +696,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -881,11 +874,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -901,8 +892,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -922,6 +912,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -932,33 +924,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2346,21 +2341,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2372,6 +2391,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2380,76 +2402,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v01-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-06-16 11:54 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index 2268cc277bc..127d4415084 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -609,11 +608,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -627,11 +623,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -705,6 +696,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -881,11 +874,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -901,8 +892,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -922,6 +912,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -932,33 +924,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2346,21 +2341,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2372,6 +2391,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2380,76 +2402,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v01-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-06-16 11:54 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index 2268cc277bc..127d4415084 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -609,11 +608,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -627,11 +623,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -705,6 +696,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -881,11 +874,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -901,8 +892,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -922,6 +912,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -932,33 +924,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2346,21 +2341,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2372,6 +2391,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2380,76 +2402,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v01-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-06-16 11:54 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index 2268cc277bc..127d4415084 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -609,11 +608,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -627,11 +623,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -705,6 +696,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -881,11 +874,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -901,8 +892,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -922,6 +912,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -932,33 +924,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2346,21 +2341,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2372,6 +2391,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2380,76 +2402,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v01-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-06-16 11:54 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index 2268cc277bc..127d4415084 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -609,11 +608,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -627,11 +623,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -705,6 +696,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -881,11 +874,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -901,8 +892,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -922,6 +912,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -932,33 +924,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2346,21 +2341,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2372,6 +2391,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2380,76 +2402,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v01-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-06-16 11:54 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index 2268cc277bc..127d4415084 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -609,11 +608,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -627,11 +623,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -705,6 +696,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -881,11 +874,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -901,8 +892,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -922,6 +912,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -932,33 +924,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2346,21 +2341,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2372,6 +2391,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2380,76 +2402,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v01-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-06-16 11:54 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index 2268cc277bc..127d4415084 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -609,11 +608,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -627,11 +623,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -705,6 +696,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -881,11 +874,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -901,8 +892,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -922,6 +912,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -932,33 +924,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2346,21 +2341,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2372,6 +2391,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2380,76 +2402,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v01-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-06-16 11:54 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index 2268cc277bc..127d4415084 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -609,11 +608,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -627,11 +623,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -705,6 +696,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -881,11 +874,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -901,8 +892,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -922,6 +912,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -932,33 +924,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2346,21 +2341,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2372,6 +2391,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2380,76 +2402,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v01-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-06-16 11:54 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index 2268cc277bc..127d4415084 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -609,11 +608,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -627,11 +623,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -705,6 +696,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -881,11 +874,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -901,8 +892,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -922,6 +912,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -932,33 +924,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2346,21 +2341,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2372,6 +2391,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2380,76 +2402,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v01-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-06-16 11:54 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index 2268cc277bc..127d4415084 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -609,11 +608,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -627,11 +623,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -705,6 +696,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -881,11 +874,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -901,8 +892,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -922,6 +912,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -932,33 +924,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2346,21 +2341,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2372,6 +2391,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2380,76 +2402,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v01-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-06-16 11:54 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index 2268cc277bc..127d4415084 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -609,11 +608,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -627,11 +623,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -705,6 +696,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -881,11 +874,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -901,8 +892,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -922,6 +912,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -932,33 +924,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2346,21 +2341,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2372,6 +2391,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2380,76 +2402,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v01-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-06-16 11:54 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index 2268cc277bc..127d4415084 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -609,11 +608,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -627,11 +623,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -705,6 +696,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -881,11 +874,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -901,8 +892,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -922,6 +912,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -932,33 +924,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2346,21 +2341,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2372,6 +2391,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2380,76 +2402,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v01-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-06-16 11:54 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index 2268cc277bc..127d4415084 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -609,11 +608,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -627,11 +623,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -705,6 +696,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -881,11 +874,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -901,8 +892,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -922,6 +912,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -932,33 +924,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2346,21 +2341,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2372,6 +2391,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2380,76 +2402,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v01-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-06-16 11:54 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index 2268cc277bc..127d4415084 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -609,11 +608,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -627,11 +623,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -705,6 +696,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -881,11 +874,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -901,8 +892,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -922,6 +912,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -932,33 +924,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2346,21 +2341,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2372,6 +2391,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2380,76 +2402,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v01-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-06-16 11:54 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index 2268cc277bc..127d4415084 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -609,11 +608,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -627,11 +623,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -705,6 +696,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -881,11 +874,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -901,8 +892,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -922,6 +912,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -932,33 +924,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2346,21 +2341,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2372,6 +2391,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2380,76 +2402,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v01-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-06-16 11:54 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index 2268cc277bc..127d4415084 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -609,11 +608,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -627,11 +623,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -705,6 +696,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -881,11 +874,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -901,8 +892,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -922,6 +912,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -932,33 +924,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2346,21 +2341,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2372,6 +2391,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2380,76 +2402,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v01-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-06-16 11:54 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index 2268cc277bc..127d4415084 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -609,11 +608,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -627,11 +623,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -705,6 +696,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -881,11 +874,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -901,8 +892,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -922,6 +912,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -932,33 +924,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2346,21 +2341,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2372,6 +2391,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2380,76 +2402,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v01-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-06-16 11:54 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index 2268cc277bc..127d4415084 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -609,11 +608,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -627,11 +623,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -705,6 +696,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -881,11 +874,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -901,8 +892,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -922,6 +912,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -932,33 +924,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2346,21 +2341,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2372,6 +2391,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2380,76 +2402,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v01-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-06-16 11:54 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index 2268cc277bc..127d4415084 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -609,11 +608,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -627,11 +623,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -705,6 +696,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -881,11 +874,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -901,8 +892,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -922,6 +912,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -932,33 +924,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2346,21 +2341,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2372,6 +2391,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2380,76 +2402,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v01-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-06-16 11:54 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index 2268cc277bc..127d4415084 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -609,11 +608,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -627,11 +623,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -705,6 +696,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -881,11 +874,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -901,8 +892,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -922,6 +912,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -932,33 +924,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2346,21 +2341,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2372,6 +2391,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2380,76 +2402,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v01-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-06-16 11:54 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index 2268cc277bc..127d4415084 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -609,11 +608,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -627,11 +623,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -705,6 +696,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -881,11 +874,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -901,8 +892,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -922,6 +912,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -932,33 +924,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2346,21 +2341,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2372,6 +2391,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2380,76 +2402,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v01-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-06-16 11:54 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index 2268cc277bc..127d4415084 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -609,11 +608,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -627,11 +623,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -705,6 +696,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -881,11 +874,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -901,8 +892,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -922,6 +912,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -932,33 +924,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2346,21 +2341,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2372,6 +2391,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2380,76 +2402,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v01-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-06-16 11:54 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index 2268cc277bc..127d4415084 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -609,11 +608,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -627,11 +623,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -705,6 +696,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -881,11 +874,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -901,8 +892,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -922,6 +912,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -932,33 +924,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2346,21 +2341,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2372,6 +2391,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2380,76 +2402,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v01-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-06-16 11:54 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index 2268cc277bc..127d4415084 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -609,11 +608,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -627,11 +623,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -705,6 +696,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -881,11 +874,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -901,8 +892,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -922,6 +912,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -932,33 +924,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2346,21 +2341,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2372,6 +2391,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2380,76 +2402,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v01-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-06-16 11:54 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index 2268cc277bc..127d4415084 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -609,11 +608,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -627,11 +623,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -705,6 +696,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -881,11 +874,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -901,8 +892,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -922,6 +912,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -932,33 +924,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2346,21 +2341,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2372,6 +2391,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2380,76 +2402,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v01-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-06-16 11:54 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index 2268cc277bc..127d4415084 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -609,11 +608,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -627,11 +623,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -705,6 +696,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -881,11 +874,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -901,8 +892,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -922,6 +912,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -932,33 +924,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2346,21 +2341,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2372,6 +2391,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2380,76 +2402,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v01-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-06-16 11:54 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index 2268cc277bc..127d4415084 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -609,11 +608,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -627,11 +623,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -705,6 +696,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -881,11 +874,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -901,8 +892,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -922,6 +912,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -932,33 +924,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2346,21 +2341,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2372,6 +2391,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2380,76 +2402,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v01-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-06-16 11:54 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index 2268cc277bc..127d4415084 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -609,11 +608,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -627,11 +623,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -705,6 +696,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -881,11 +874,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -901,8 +892,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -922,6 +912,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -932,33 +924,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2346,21 +2341,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2372,6 +2391,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2380,76 +2402,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v01-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-06-16 11:54 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index 2268cc277bc..127d4415084 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -609,11 +608,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -627,11 +623,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -705,6 +696,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -881,11 +874,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -901,8 +892,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -922,6 +912,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -932,33 +924,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2346,21 +2341,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2372,6 +2391,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2380,76 +2402,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v01-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-06-16 11:54 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index 2268cc277bc..127d4415084 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -609,11 +608,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -627,11 +623,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -705,6 +696,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -881,11 +874,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -901,8 +892,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -922,6 +912,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -932,33 +924,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2346,21 +2341,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2372,6 +2391,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2380,76 +2402,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v01-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-06-16 11:54 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index 2268cc277bc..127d4415084 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -609,11 +608,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -627,11 +623,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -705,6 +696,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -881,11 +874,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -901,8 +892,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -922,6 +912,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -932,33 +924,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2346,21 +2341,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2372,6 +2391,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2380,76 +2402,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v01-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-06-16 11:54 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index 2268cc277bc..127d4415084 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -609,11 +608,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -627,11 +623,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -705,6 +696,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -881,11 +874,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -901,8 +892,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -922,6 +912,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -932,33 +924,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2346,21 +2341,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2372,6 +2391,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2380,76 +2402,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v01-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-06-16 11:54 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index 2268cc277bc..127d4415084 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -609,11 +608,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -627,11 +623,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -705,6 +696,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -881,11 +874,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -901,8 +892,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -922,6 +912,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -932,33 +924,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2346,21 +2341,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2372,6 +2391,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2380,76 +2402,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v01-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-06-16 11:54 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index 2268cc277bc..127d4415084 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -609,11 +608,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -627,11 +623,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -705,6 +696,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -881,11 +874,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -901,8 +892,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -922,6 +912,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -932,33 +924,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2346,21 +2341,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2372,6 +2391,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2380,76 +2402,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v01-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-06-16 11:54 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index 2268cc277bc..127d4415084 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -609,11 +608,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -627,11 +623,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -705,6 +696,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -881,11 +874,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -901,8 +892,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -922,6 +912,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -932,33 +924,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2346,21 +2341,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2372,6 +2391,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2380,76 +2402,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v01-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-06-16 11:54 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index 2268cc277bc..127d4415084 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -609,11 +608,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -627,11 +623,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -705,6 +696,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -881,11 +874,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -901,8 +892,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -922,6 +912,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -932,33 +924,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2346,21 +2341,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2372,6 +2391,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2380,76 +2402,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v01-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-06-16 11:54 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index 2268cc277bc..127d4415084 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -609,11 +608,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -627,11 +623,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -705,6 +696,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -881,11 +874,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -901,8 +892,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -922,6 +912,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -932,33 +924,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2346,21 +2341,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2372,6 +2391,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2380,76 +2402,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v01-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-06-16 11:54 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index 2268cc277bc..127d4415084 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -609,11 +608,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -627,11 +623,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -705,6 +696,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -881,11 +874,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -901,8 +892,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -922,6 +912,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -932,33 +924,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2346,21 +2341,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2372,6 +2391,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2380,76 +2402,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v01-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-06-16 11:54 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index 2268cc277bc..127d4415084 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -609,11 +608,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -627,11 +623,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -705,6 +696,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -881,11 +874,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -901,8 +892,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -922,6 +912,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -932,33 +924,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2346,21 +2341,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2372,6 +2391,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2380,76 +2402,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v01-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-06-16 11:54 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index 2268cc277bc..127d4415084 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -609,11 +608,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -627,11 +623,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -705,6 +696,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -881,11 +874,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -901,8 +892,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -922,6 +912,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -932,33 +924,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2346,21 +2341,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2372,6 +2391,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2380,76 +2402,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v01-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-06-16 11:54 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index 2268cc277bc..127d4415084 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -609,11 +608,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -627,11 +623,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -705,6 +696,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -881,11 +874,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -901,8 +892,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -922,6 +912,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -932,33 +924,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2346,21 +2341,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2372,6 +2391,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2380,76 +2402,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v01-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-06-16 11:54 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index 2268cc277bc..127d4415084 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -609,11 +608,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -627,11 +623,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -705,6 +696,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -881,11 +874,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -901,8 +892,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -922,6 +912,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -932,33 +924,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2346,21 +2341,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2372,6 +2391,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2380,76 +2402,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v01-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-06-16 11:54 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index 2268cc277bc..127d4415084 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -609,11 +608,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -627,11 +623,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -705,6 +696,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -881,11 +874,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -901,8 +892,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -922,6 +912,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -932,33 +924,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2346,21 +2341,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2372,6 +2391,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2380,76 +2402,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v01-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-06-16 11:54 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index 2268cc277bc..127d4415084 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -609,11 +608,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -627,11 +623,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -705,6 +696,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -881,11 +874,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -901,8 +892,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -922,6 +912,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -932,33 +924,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2346,21 +2341,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2372,6 +2391,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2380,76 +2402,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v01-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-06-16 11:54 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index 2268cc277bc..127d4415084 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -609,11 +608,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -627,11 +623,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -705,6 +696,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -881,11 +874,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -901,8 +892,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -922,6 +912,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -932,33 +924,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2346,21 +2341,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2372,6 +2391,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2380,76 +2402,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v01-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-06-16 11:54 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index 2268cc277bc..127d4415084 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -609,11 +608,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -627,11 +623,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -705,6 +696,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -881,11 +874,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -901,8 +892,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -922,6 +912,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -932,33 +924,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2346,21 +2341,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2372,6 +2391,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2380,76 +2402,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v01-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-06-16 11:54 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index 2268cc277bc..127d4415084 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -609,11 +608,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -627,11 +623,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -705,6 +696,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -881,11 +874,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -901,8 +892,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -922,6 +912,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -932,33 +924,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2346,21 +2341,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2372,6 +2391,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2380,76 +2402,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v01-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-06-16 11:54 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index 2268cc277bc..127d4415084 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -609,11 +608,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -627,11 +623,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -705,6 +696,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -881,11 +874,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -901,8 +892,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -922,6 +912,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -932,33 +924,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2346,21 +2341,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2372,6 +2391,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2380,76 +2402,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v01-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-06-16 11:54 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index 2268cc277bc..127d4415084 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -609,11 +608,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -627,11 +623,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -705,6 +696,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -881,11 +874,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -901,8 +892,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -922,6 +912,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -932,33 +924,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2346,21 +2341,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2372,6 +2391,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2380,76 +2402,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v01-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-06-16 11:54 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index 2268cc277bc..127d4415084 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -609,11 +608,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -627,11 +623,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -705,6 +696,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -881,11 +874,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -901,8 +892,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -922,6 +912,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -932,33 +924,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2346,21 +2341,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2372,6 +2391,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2380,76 +2402,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v01-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-06-16 11:54 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index 2268cc277bc..127d4415084 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -609,11 +608,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -627,11 +623,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -705,6 +696,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -881,11 +874,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -901,8 +892,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -922,6 +912,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -932,33 +924,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2346,21 +2341,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2372,6 +2391,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2380,76 +2402,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v01-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-06-16 11:54 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index 2268cc277bc..127d4415084 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -609,11 +608,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -627,11 +623,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -705,6 +696,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -881,11 +874,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -901,8 +892,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -922,6 +912,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -932,33 +924,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2346,21 +2341,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2372,6 +2391,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2380,76 +2402,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v01-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-06-16 11:54 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index 2268cc277bc..127d4415084 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -609,11 +608,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -627,11 +623,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -705,6 +696,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -881,11 +874,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -901,8 +892,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -922,6 +912,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -932,33 +924,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2346,21 +2341,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2372,6 +2391,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2380,76 +2402,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v01-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-06-16 11:54 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index 2268cc277bc..127d4415084 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -609,11 +608,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -627,11 +623,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -705,6 +696,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -881,11 +874,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -901,8 +892,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -922,6 +912,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -932,33 +924,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2346,21 +2341,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2372,6 +2391,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2380,76 +2402,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v01-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-06-16 11:54 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index 2268cc277bc..127d4415084 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -609,11 +608,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -627,11 +623,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -705,6 +696,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -881,11 +874,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -901,8 +892,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -922,6 +912,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -932,33 +924,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2346,21 +2341,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2372,6 +2391,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2380,76 +2402,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v01-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-06-16 11:54 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index 2268cc277bc..127d4415084 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -609,11 +608,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -627,11 +623,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -705,6 +696,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -881,11 +874,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -901,8 +892,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -922,6 +912,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -932,33 +924,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2346,21 +2341,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2372,6 +2391,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2380,76 +2402,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v01-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-06-16 11:54 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index 2268cc277bc..127d4415084 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -609,11 +608,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -627,11 +623,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -705,6 +696,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -881,11 +874,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -901,8 +892,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -922,6 +912,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -932,33 +924,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2346,21 +2341,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2372,6 +2391,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2380,76 +2402,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v01-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-06-16 11:54 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index 2268cc277bc..127d4415084 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -609,11 +608,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -627,11 +623,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -705,6 +696,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -881,11 +874,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -901,8 +892,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -922,6 +912,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -932,33 +924,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2346,21 +2341,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2372,6 +2391,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2380,76 +2402,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v01-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-06-16 11:54 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index 2268cc277bc..127d4415084 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -609,11 +608,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -627,11 +623,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -705,6 +696,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -881,11 +874,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -901,8 +892,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -922,6 +912,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -932,33 +924,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2346,21 +2341,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2372,6 +2391,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2380,76 +2402,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v01-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-06-16 11:54 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index 2268cc277bc..127d4415084 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -609,11 +608,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -627,11 +623,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -705,6 +696,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -881,11 +874,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -901,8 +892,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -922,6 +912,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -932,33 +924,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2346,21 +2341,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2372,6 +2391,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2380,76 +2402,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v01-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-06-16 11:54 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index 2268cc277bc..127d4415084 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -609,11 +608,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -627,11 +623,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -705,6 +696,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -881,11 +874,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -901,8 +892,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -922,6 +912,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -932,33 +924,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2346,21 +2341,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2372,6 +2391,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2380,76 +2402,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v01-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-06-16 11:54 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index 2268cc277bc..127d4415084 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -609,11 +608,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -627,11 +623,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -705,6 +696,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -881,11 +874,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -901,8 +892,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -922,6 +912,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -932,33 +924,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2346,21 +2341,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2372,6 +2391,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2380,76 +2402,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v01-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-06-16 11:54 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index 2268cc277bc..127d4415084 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -609,11 +608,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -627,11 +623,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -705,6 +696,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -881,11 +874,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -901,8 +892,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -922,6 +912,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -932,33 +924,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2346,21 +2341,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2372,6 +2391,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2380,76 +2402,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v01-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-06-16 11:54 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index 2268cc277bc..127d4415084 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -609,11 +608,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -627,11 +623,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -705,6 +696,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -881,11 +874,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -901,8 +892,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -922,6 +912,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -932,33 +924,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2346,21 +2341,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2372,6 +2391,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2380,76 +2402,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v01-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-06-16 11:54 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index 2268cc277bc..127d4415084 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -609,11 +608,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -627,11 +623,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -705,6 +696,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -881,11 +874,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -901,8 +892,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -922,6 +912,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -932,33 +924,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2346,21 +2341,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2372,6 +2391,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2380,76 +2402,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v01-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-06-16 11:54 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index 2268cc277bc..127d4415084 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -609,11 +608,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -627,11 +623,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -705,6 +696,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -881,11 +874,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -901,8 +892,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -922,6 +912,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -932,33 +924,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2346,21 +2341,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2372,6 +2391,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2380,76 +2402,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v01-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-06-16 11:54 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index 2268cc277bc..127d4415084 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -609,11 +608,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -627,11 +623,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -705,6 +696,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -881,11 +874,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -901,8 +892,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -922,6 +912,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -932,33 +924,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2346,21 +2341,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2372,6 +2391,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2380,76 +2402,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v01-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-06-16 11:54 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index 2268cc277bc..127d4415084 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -609,11 +608,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -627,11 +623,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -705,6 +696,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -881,11 +874,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -901,8 +892,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -922,6 +912,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -932,33 +924,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2346,21 +2341,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2372,6 +2391,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2380,76 +2402,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v01-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-06-16 11:54 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index 2268cc277bc..127d4415084 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -609,11 +608,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -627,11 +623,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -705,6 +696,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -881,11 +874,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -901,8 +892,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -922,6 +912,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -932,33 +924,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2346,21 +2341,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2372,6 +2391,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2380,76 +2402,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v01-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-06-16 11:54 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index 2268cc277bc..127d4415084 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -609,11 +608,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -627,11 +623,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -705,6 +696,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -881,11 +874,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -901,8 +892,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -922,6 +912,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -932,33 +924,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2346,21 +2341,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2372,6 +2391,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2380,76 +2402,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v01-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-06-16 11:54 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index 2268cc277bc..127d4415084 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -609,11 +608,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -627,11 +623,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -705,6 +696,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -881,11 +874,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -901,8 +892,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -922,6 +912,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -932,33 +924,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2346,21 +2341,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2372,6 +2391,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2380,76 +2402,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v01-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-06-16 11:54 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index 2268cc277bc..127d4415084 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -609,11 +608,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -627,11 +623,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -705,6 +696,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -881,11 +874,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -901,8 +892,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -922,6 +912,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -932,33 +924,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2346,21 +2341,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2372,6 +2391,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2380,76 +2402,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v01-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-06-16 11:54 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index 2268cc277bc..127d4415084 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -609,11 +608,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -627,11 +623,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -705,6 +696,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -881,11 +874,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -901,8 +892,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -922,6 +912,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -932,33 +924,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2346,21 +2341,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2372,6 +2391,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2380,76 +2402,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v01-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-06-16 11:54 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index 2268cc277bc..127d4415084 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -609,11 +608,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -627,11 +623,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -705,6 +696,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -881,11 +874,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -901,8 +892,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -922,6 +912,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -932,33 +924,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2346,21 +2341,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2372,6 +2391,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2380,76 +2402,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v01-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-06-16 11:54 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index 2268cc277bc..127d4415084 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -609,11 +608,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -627,11 +623,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -705,6 +696,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -881,11 +874,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -901,8 +892,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -922,6 +912,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -932,33 +924,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2346,21 +2341,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2372,6 +2391,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2380,76 +2402,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v01-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-06-16 11:54 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-06-16 11:54 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index 2268cc277bc..127d4415084 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -609,11 +608,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -627,11 +623,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -705,6 +696,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -881,11 +874,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -901,8 +892,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -922,6 +912,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -932,33 +924,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2346,21 +2341,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2372,6 +2391,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2380,76 +2402,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v01-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-07-15 08:37 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index bf87430cf01..4db8a16f691 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -603,11 +602,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -621,11 +617,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -699,6 +690,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -875,11 +868,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -895,8 +886,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -916,6 +906,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -926,33 +918,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2340,21 +2335,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2366,6 +2385,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2374,76 +2396,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v02-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-07-15 08:37 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index bf87430cf01..4db8a16f691 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -603,11 +602,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -621,11 +617,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -699,6 +690,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -875,11 +868,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -895,8 +886,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -916,6 +906,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -926,33 +918,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2340,21 +2335,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2366,6 +2385,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2374,76 +2396,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v02-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-07-15 08:37 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index bf87430cf01..4db8a16f691 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -603,11 +602,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -621,11 +617,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -699,6 +690,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -875,11 +868,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -895,8 +886,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -916,6 +906,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -926,33 +918,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2340,21 +2335,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2366,6 +2385,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2374,76 +2396,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v02-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-07-15 08:37 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index bf87430cf01..4db8a16f691 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -603,11 +602,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -621,11 +617,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -699,6 +690,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -875,11 +868,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -895,8 +886,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -916,6 +906,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -926,33 +918,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2340,21 +2335,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2366,6 +2385,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2374,76 +2396,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v02-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-07-15 08:37 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index bf87430cf01..4db8a16f691 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -603,11 +602,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -621,11 +617,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -699,6 +690,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -875,11 +868,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -895,8 +886,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -916,6 +906,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -926,33 +918,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2340,21 +2335,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2366,6 +2385,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2374,76 +2396,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v02-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-07-15 08:37 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index bf87430cf01..4db8a16f691 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -603,11 +602,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -621,11 +617,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -699,6 +690,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -875,11 +868,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -895,8 +886,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -916,6 +906,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -926,33 +918,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2340,21 +2335,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2366,6 +2385,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2374,76 +2396,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v02-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-07-15 08:37 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index bf87430cf01..4db8a16f691 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -603,11 +602,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -621,11 +617,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -699,6 +690,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -875,11 +868,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -895,8 +886,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -916,6 +906,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -926,33 +918,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2340,21 +2335,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2366,6 +2385,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2374,76 +2396,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v02-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-07-15 08:37 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index bf87430cf01..4db8a16f691 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -603,11 +602,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -621,11 +617,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -699,6 +690,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -875,11 +868,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -895,8 +886,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -916,6 +906,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -926,33 +918,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2340,21 +2335,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2366,6 +2385,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2374,76 +2396,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v02-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-07-15 08:37 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index bf87430cf01..4db8a16f691 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -603,11 +602,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -621,11 +617,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -699,6 +690,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -875,11 +868,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -895,8 +886,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -916,6 +906,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -926,33 +918,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2340,21 +2335,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2366,6 +2385,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2374,76 +2396,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v02-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-07-15 08:37 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index bf87430cf01..4db8a16f691 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -603,11 +602,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -621,11 +617,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -699,6 +690,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -875,11 +868,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -895,8 +886,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -916,6 +906,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -926,33 +918,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2340,21 +2335,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2366,6 +2385,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2374,76 +2396,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v02-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-07-15 08:37 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index bf87430cf01..4db8a16f691 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -603,11 +602,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -621,11 +617,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -699,6 +690,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -875,11 +868,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -895,8 +886,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -916,6 +906,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -926,33 +918,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2340,21 +2335,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2366,6 +2385,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2374,76 +2396,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v02-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-07-15 08:37 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index bf87430cf01..4db8a16f691 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -603,11 +602,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -621,11 +617,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -699,6 +690,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -875,11 +868,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -895,8 +886,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -916,6 +906,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -926,33 +918,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2340,21 +2335,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2366,6 +2385,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2374,76 +2396,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v02-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-07-15 08:37 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index bf87430cf01..4db8a16f691 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -603,11 +602,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -621,11 +617,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -699,6 +690,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -875,11 +868,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -895,8 +886,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -916,6 +906,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -926,33 +918,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2340,21 +2335,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2366,6 +2385,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2374,76 +2396,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v02-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-07-15 08:37 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index bf87430cf01..4db8a16f691 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -603,11 +602,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -621,11 +617,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -699,6 +690,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -875,11 +868,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -895,8 +886,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -916,6 +906,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -926,33 +918,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2340,21 +2335,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2366,6 +2385,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2374,76 +2396,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v02-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-07-15 08:37 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index bf87430cf01..4db8a16f691 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -603,11 +602,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -621,11 +617,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -699,6 +690,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -875,11 +868,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -895,8 +886,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -916,6 +906,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -926,33 +918,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2340,21 +2335,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2366,6 +2385,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2374,76 +2396,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v02-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-07-15 08:37 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index bf87430cf01..4db8a16f691 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -603,11 +602,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -621,11 +617,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -699,6 +690,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -875,11 +868,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -895,8 +886,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -916,6 +906,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -926,33 +918,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2340,21 +2335,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2366,6 +2385,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2374,76 +2396,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v02-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-07-15 08:37 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index bf87430cf01..4db8a16f691 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -603,11 +602,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -621,11 +617,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -699,6 +690,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -875,11 +868,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -895,8 +886,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -916,6 +906,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -926,33 +918,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2340,21 +2335,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2366,6 +2385,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2374,76 +2396,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v02-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-07-15 08:37 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index bf87430cf01..4db8a16f691 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -603,11 +602,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -621,11 +617,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -699,6 +690,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -875,11 +868,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -895,8 +886,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -916,6 +906,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -926,33 +918,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2340,21 +2335,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2366,6 +2385,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2374,76 +2396,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v02-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-07-15 08:37 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index bf87430cf01..4db8a16f691 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -603,11 +602,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -621,11 +617,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -699,6 +690,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -875,11 +868,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -895,8 +886,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -916,6 +906,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -926,33 +918,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2340,21 +2335,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2366,6 +2385,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2374,76 +2396,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v02-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-07-15 08:37 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index bf87430cf01..4db8a16f691 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -603,11 +602,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -621,11 +617,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -699,6 +690,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -875,11 +868,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -895,8 +886,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -916,6 +906,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -926,33 +918,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2340,21 +2335,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2366,6 +2385,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2374,76 +2396,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v02-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-07-15 08:37 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index bf87430cf01..4db8a16f691 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -603,11 +602,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -621,11 +617,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -699,6 +690,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -875,11 +868,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -895,8 +886,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -916,6 +906,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -926,33 +918,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2340,21 +2335,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2366,6 +2385,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2374,76 +2396,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v02-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-07-15 08:37 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index bf87430cf01..4db8a16f691 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -603,11 +602,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -621,11 +617,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -699,6 +690,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -875,11 +868,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -895,8 +886,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -916,6 +906,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -926,33 +918,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2340,21 +2335,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2366,6 +2385,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2374,76 +2396,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v02-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-07-15 08:37 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index bf87430cf01..4db8a16f691 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -603,11 +602,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -621,11 +617,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -699,6 +690,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -875,11 +868,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -895,8 +886,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -916,6 +906,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -926,33 +918,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2340,21 +2335,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2366,6 +2385,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2374,76 +2396,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v02-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-07-15 08:37 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index bf87430cf01..4db8a16f691 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -603,11 +602,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -621,11 +617,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -699,6 +690,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -875,11 +868,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -895,8 +886,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -916,6 +906,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -926,33 +918,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2340,21 +2335,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2366,6 +2385,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2374,76 +2396,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v02-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-07-15 08:37 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index bf87430cf01..4db8a16f691 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -603,11 +602,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -621,11 +617,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -699,6 +690,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -875,11 +868,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -895,8 +886,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -916,6 +906,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -926,33 +918,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2340,21 +2335,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2366,6 +2385,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2374,76 +2396,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v02-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-07-15 08:37 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index bf87430cf01..4db8a16f691 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -603,11 +602,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -621,11 +617,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -699,6 +690,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -875,11 +868,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -895,8 +886,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -916,6 +906,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -926,33 +918,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2340,21 +2335,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2366,6 +2385,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2374,76 +2396,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v02-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-07-15 08:37 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index bf87430cf01..4db8a16f691 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -603,11 +602,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -621,11 +617,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -699,6 +690,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -875,11 +868,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -895,8 +886,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -916,6 +906,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -926,33 +918,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2340,21 +2335,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2366,6 +2385,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2374,76 +2396,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v02-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-07-15 08:37 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index bf87430cf01..4db8a16f691 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -603,11 +602,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -621,11 +617,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -699,6 +690,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -875,11 +868,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -895,8 +886,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -916,6 +906,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -926,33 +918,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2340,21 +2335,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2366,6 +2385,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2374,76 +2396,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v02-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-07-15 08:37 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index bf87430cf01..4db8a16f691 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -603,11 +602,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -621,11 +617,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -699,6 +690,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -875,11 +868,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -895,8 +886,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -916,6 +906,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -926,33 +918,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2340,21 +2335,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2366,6 +2385,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2374,76 +2396,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v02-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-07-15 08:37 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index bf87430cf01..4db8a16f691 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -603,11 +602,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -621,11 +617,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -699,6 +690,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -875,11 +868,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -895,8 +886,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -916,6 +906,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -926,33 +918,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2340,21 +2335,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2366,6 +2385,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2374,76 +2396,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v02-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-07-15 08:37 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index bf87430cf01..4db8a16f691 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -603,11 +602,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -621,11 +617,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -699,6 +690,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -875,11 +868,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -895,8 +886,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -916,6 +906,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -926,33 +918,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2340,21 +2335,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2366,6 +2385,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2374,76 +2396,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v02-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-07-15 08:37 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index bf87430cf01..4db8a16f691 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -603,11 +602,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -621,11 +617,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -699,6 +690,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -875,11 +868,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -895,8 +886,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -916,6 +906,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -926,33 +918,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2340,21 +2335,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2366,6 +2385,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2374,76 +2396,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v02-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-07-15 08:37 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index bf87430cf01..4db8a16f691 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -603,11 +602,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -621,11 +617,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -699,6 +690,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -875,11 +868,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -895,8 +886,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -916,6 +906,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -926,33 +918,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2340,21 +2335,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2366,6 +2385,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2374,76 +2396,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v02-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-07-15 08:37 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index bf87430cf01..4db8a16f691 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -603,11 +602,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -621,11 +617,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -699,6 +690,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -875,11 +868,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -895,8 +886,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -916,6 +906,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -926,33 +918,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2340,21 +2335,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2366,6 +2385,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2374,76 +2396,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v02-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-07-15 08:37 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index bf87430cf01..4db8a16f691 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -603,11 +602,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -621,11 +617,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -699,6 +690,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -875,11 +868,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -895,8 +886,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -916,6 +906,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -926,33 +918,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2340,21 +2335,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2366,6 +2385,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2374,76 +2396,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v02-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-07-15 08:37 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index bf87430cf01..4db8a16f691 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -603,11 +602,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -621,11 +617,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -699,6 +690,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -875,11 +868,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -895,8 +886,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -916,6 +906,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -926,33 +918,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2340,21 +2335,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2366,6 +2385,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2374,76 +2396,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v02-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-07-15 08:37 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index bf87430cf01..4db8a16f691 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -603,11 +602,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -621,11 +617,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -699,6 +690,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -875,11 +868,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -895,8 +886,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -916,6 +906,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -926,33 +918,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2340,21 +2335,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2366,6 +2385,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2374,76 +2396,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v02-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-07-15 08:37 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index bf87430cf01..4db8a16f691 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -603,11 +602,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -621,11 +617,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -699,6 +690,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -875,11 +868,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -895,8 +886,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -916,6 +906,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -926,33 +918,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2340,21 +2335,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2366,6 +2385,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2374,76 +2396,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v02-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-07-15 08:37 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index bf87430cf01..4db8a16f691 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -603,11 +602,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -621,11 +617,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -699,6 +690,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -875,11 +868,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -895,8 +886,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -916,6 +906,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -926,33 +918,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2340,21 +2335,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2366,6 +2385,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2374,76 +2396,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v02-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-07-15 08:37 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index bf87430cf01..4db8a16f691 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -603,11 +602,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -621,11 +617,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -699,6 +690,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -875,11 +868,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -895,8 +886,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -916,6 +906,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -926,33 +918,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2340,21 +2335,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2366,6 +2385,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2374,76 +2396,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v02-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-07-15 08:37 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index bf87430cf01..4db8a16f691 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -603,11 +602,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -621,11 +617,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -699,6 +690,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -875,11 +868,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -895,8 +886,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -916,6 +906,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -926,33 +918,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2340,21 +2335,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2366,6 +2385,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2374,76 +2396,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v02-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-07-15 08:37 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index bf87430cf01..4db8a16f691 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -603,11 +602,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -621,11 +617,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -699,6 +690,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -875,11 +868,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -895,8 +886,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -916,6 +906,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -926,33 +918,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2340,21 +2335,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2366,6 +2385,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2374,76 +2396,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v02-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-07-15 08:37 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index bf87430cf01..4db8a16f691 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -603,11 +602,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -621,11 +617,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -699,6 +690,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -875,11 +868,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -895,8 +886,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -916,6 +906,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -926,33 +918,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2340,21 +2335,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2366,6 +2385,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2374,76 +2396,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v02-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-07-15 08:37 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index bf87430cf01..4db8a16f691 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -603,11 +602,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -621,11 +617,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -699,6 +690,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -875,11 +868,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -895,8 +886,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -916,6 +906,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -926,33 +918,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2340,21 +2335,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2366,6 +2385,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2374,76 +2396,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v02-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-07-15 08:37 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index bf87430cf01..4db8a16f691 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -603,11 +602,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -621,11 +617,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -699,6 +690,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -875,11 +868,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -895,8 +886,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -916,6 +906,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -926,33 +918,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2340,21 +2335,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2366,6 +2385,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2374,76 +2396,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v02-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-07-15 08:37 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index bf87430cf01..4db8a16f691 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -603,11 +602,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -621,11 +617,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -699,6 +690,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -875,11 +868,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -895,8 +886,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -916,6 +906,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -926,33 +918,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2340,21 +2335,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2366,6 +2385,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2374,76 +2396,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v02-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-07-15 08:37 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index bf87430cf01..4db8a16f691 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -603,11 +602,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -621,11 +617,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -699,6 +690,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -875,11 +868,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -895,8 +886,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -916,6 +906,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -926,33 +918,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2340,21 +2335,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2366,6 +2385,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2374,76 +2396,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v02-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-07-15 08:37 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index bf87430cf01..4db8a16f691 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -603,11 +602,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -621,11 +617,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -699,6 +690,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -875,11 +868,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -895,8 +886,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -916,6 +906,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -926,33 +918,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2340,21 +2335,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2366,6 +2385,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2374,76 +2396,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v02-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-07-15 08:37 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index bf87430cf01..4db8a16f691 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -603,11 +602,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -621,11 +617,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -699,6 +690,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -875,11 +868,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -895,8 +886,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -916,6 +906,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -926,33 +918,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2340,21 +2335,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2366,6 +2385,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2374,76 +2396,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v02-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-07-15 08:37 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index bf87430cf01..4db8a16f691 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -603,11 +602,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -621,11 +617,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -699,6 +690,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -875,11 +868,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -895,8 +886,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -916,6 +906,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -926,33 +918,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2340,21 +2335,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2366,6 +2385,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2374,76 +2396,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v02-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-07-15 08:37 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index bf87430cf01..4db8a16f691 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -603,11 +602,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -621,11 +617,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -699,6 +690,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -875,11 +868,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -895,8 +886,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -916,6 +906,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -926,33 +918,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2340,21 +2335,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2366,6 +2385,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2374,76 +2396,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v02-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-07-15 08:37 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index bf87430cf01..4db8a16f691 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -603,11 +602,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -621,11 +617,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -699,6 +690,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -875,11 +868,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -895,8 +886,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -916,6 +906,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -926,33 +918,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2340,21 +2335,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2366,6 +2385,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2374,76 +2396,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v02-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-07-15 08:37 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index bf87430cf01..4db8a16f691 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -603,11 +602,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -621,11 +617,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -699,6 +690,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -875,11 +868,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -895,8 +886,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -916,6 +906,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -926,33 +918,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2340,21 +2335,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2366,6 +2385,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2374,76 +2396,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v02-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-07-15 08:37 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index bf87430cf01..4db8a16f691 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -603,11 +602,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -621,11 +617,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -699,6 +690,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -875,11 +868,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -895,8 +886,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -916,6 +906,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -926,33 +918,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2340,21 +2335,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2366,6 +2385,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2374,76 +2396,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v02-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-07-15 08:37 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index bf87430cf01..4db8a16f691 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -603,11 +602,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -621,11 +617,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -699,6 +690,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -875,11 +868,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -895,8 +886,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -916,6 +906,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -926,33 +918,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2340,21 +2335,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2366,6 +2385,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2374,76 +2396,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v02-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-07-15 08:37 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index bf87430cf01..4db8a16f691 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -603,11 +602,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -621,11 +617,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -699,6 +690,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -875,11 +868,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -895,8 +886,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -916,6 +906,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -926,33 +918,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2340,21 +2335,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2366,6 +2385,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2374,76 +2396,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v02-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-07-15 08:37 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index bf87430cf01..4db8a16f691 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -603,11 +602,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -621,11 +617,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -699,6 +690,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -875,11 +868,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -895,8 +886,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -916,6 +906,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -926,33 +918,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2340,21 +2335,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2366,6 +2385,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2374,76 +2396,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v02-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-07-15 08:37 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index bf87430cf01..4db8a16f691 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -603,11 +602,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -621,11 +617,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -699,6 +690,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -875,11 +868,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -895,8 +886,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -916,6 +906,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -926,33 +918,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2340,21 +2335,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2366,6 +2385,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2374,76 +2396,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v02-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-07-15 08:37 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index bf87430cf01..4db8a16f691 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -603,11 +602,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -621,11 +617,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -699,6 +690,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -875,11 +868,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -895,8 +886,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -916,6 +906,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -926,33 +918,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2340,21 +2335,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2366,6 +2385,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2374,76 +2396,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v02-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-07-15 08:37 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index bf87430cf01..4db8a16f691 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -603,11 +602,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -621,11 +617,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -699,6 +690,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -875,11 +868,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -895,8 +886,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -916,6 +906,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -926,33 +918,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2340,21 +2335,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2366,6 +2385,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2374,76 +2396,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v02-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-07-15 08:37 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index bf87430cf01..4db8a16f691 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -603,11 +602,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -621,11 +617,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -699,6 +690,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -875,11 +868,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -895,8 +886,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -916,6 +906,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -926,33 +918,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2340,21 +2335,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2366,6 +2385,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2374,76 +2396,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v02-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-07-15 08:37 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index bf87430cf01..4db8a16f691 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -603,11 +602,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -621,11 +617,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -699,6 +690,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -875,11 +868,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -895,8 +886,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -916,6 +906,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -926,33 +918,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2340,21 +2335,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2366,6 +2385,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2374,76 +2396,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v02-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-07-15 08:37 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index bf87430cf01..4db8a16f691 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -603,11 +602,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -621,11 +617,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -699,6 +690,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -875,11 +868,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -895,8 +886,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -916,6 +906,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -926,33 +918,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2340,21 +2335,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2366,6 +2385,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2374,76 +2396,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v02-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-07-15 08:37 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index bf87430cf01..4db8a16f691 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -603,11 +602,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -621,11 +617,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -699,6 +690,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -875,11 +868,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -895,8 +886,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -916,6 +906,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -926,33 +918,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2340,21 +2335,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2366,6 +2385,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2374,76 +2396,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v02-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-07-15 08:37 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index bf87430cf01..4db8a16f691 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -603,11 +602,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -621,11 +617,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -699,6 +690,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -875,11 +868,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -895,8 +886,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -916,6 +906,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -926,33 +918,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2340,21 +2335,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2366,6 +2385,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2374,76 +2396,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v02-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-07-15 08:37 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index bf87430cf01..4db8a16f691 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -603,11 +602,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -621,11 +617,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -699,6 +690,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -875,11 +868,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -895,8 +886,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -916,6 +906,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -926,33 +918,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2340,21 +2335,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2366,6 +2385,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2374,76 +2396,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v02-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-07-15 08:37 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index bf87430cf01..4db8a16f691 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -603,11 +602,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -621,11 +617,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -699,6 +690,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -875,11 +868,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -895,8 +886,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -916,6 +906,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -926,33 +918,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2340,21 +2335,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2366,6 +2385,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2374,76 +2396,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v02-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-07-15 08:37 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index bf87430cf01..4db8a16f691 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -603,11 +602,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -621,11 +617,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -699,6 +690,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -875,11 +868,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -895,8 +886,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -916,6 +906,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -926,33 +918,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2340,21 +2335,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2366,6 +2385,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2374,76 +2396,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v02-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-07-15 08:37 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index bf87430cf01..4db8a16f691 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -603,11 +602,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -621,11 +617,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -699,6 +690,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -875,11 +868,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -895,8 +886,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -916,6 +906,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -926,33 +918,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2340,21 +2335,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2366,6 +2385,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2374,76 +2396,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v02-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-07-15 08:37 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index bf87430cf01..4db8a16f691 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -603,11 +602,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -621,11 +617,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -699,6 +690,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -875,11 +868,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -895,8 +886,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -916,6 +906,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -926,33 +918,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2340,21 +2335,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2366,6 +2385,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2374,76 +2396,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v02-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-07-15 08:37 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index bf87430cf01..4db8a16f691 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -603,11 +602,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -621,11 +617,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -699,6 +690,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -875,11 +868,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -895,8 +886,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -916,6 +906,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -926,33 +918,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2340,21 +2335,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2366,6 +2385,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2374,76 +2396,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v02-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-07-15 08:37 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index bf87430cf01..4db8a16f691 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -603,11 +602,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -621,11 +617,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -699,6 +690,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -875,11 +868,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -895,8 +886,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -916,6 +906,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -926,33 +918,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2340,21 +2335,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2366,6 +2385,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2374,76 +2396,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v02-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-07-15 08:37 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index bf87430cf01..4db8a16f691 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -603,11 +602,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -621,11 +617,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -699,6 +690,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -875,11 +868,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -895,8 +886,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -916,6 +906,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -926,33 +918,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2340,21 +2335,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2366,6 +2385,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2374,76 +2396,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v02-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-07-15 08:37 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index bf87430cf01..4db8a16f691 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -603,11 +602,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -621,11 +617,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -699,6 +690,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -875,11 +868,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -895,8 +886,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -916,6 +906,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -926,33 +918,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2340,21 +2335,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2366,6 +2385,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2374,76 +2396,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v02-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-07-15 08:37 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index bf87430cf01..4db8a16f691 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -603,11 +602,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -621,11 +617,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -699,6 +690,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -875,11 +868,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -895,8 +886,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -916,6 +906,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -926,33 +918,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2340,21 +2335,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2366,6 +2385,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2374,76 +2396,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v02-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-07-15 08:37 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index bf87430cf01..4db8a16f691 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -603,11 +602,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -621,11 +617,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -699,6 +690,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -875,11 +868,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -895,8 +886,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -916,6 +906,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -926,33 +918,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2340,21 +2335,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2366,6 +2385,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2374,76 +2396,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v02-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-07-15 08:37 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index bf87430cf01..4db8a16f691 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -603,11 +602,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -621,11 +617,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -699,6 +690,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -875,11 +868,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -895,8 +886,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -916,6 +906,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -926,33 +918,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2340,21 +2335,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2366,6 +2385,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2374,76 +2396,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v02-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-07-15 08:37 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index bf87430cf01..4db8a16f691 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -603,11 +602,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -621,11 +617,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -699,6 +690,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -875,11 +868,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -895,8 +886,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -916,6 +906,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -926,33 +918,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2340,21 +2335,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2366,6 +2385,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2374,76 +2396,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v02-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-07-15 08:37 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index bf87430cf01..4db8a16f691 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -603,11 +602,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -621,11 +617,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -699,6 +690,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -875,11 +868,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -895,8 +886,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -916,6 +906,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -926,33 +918,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2340,21 +2335,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2366,6 +2385,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2374,76 +2396,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v02-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-07-15 08:37 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index bf87430cf01..4db8a16f691 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -603,11 +602,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -621,11 +617,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -699,6 +690,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -875,11 +868,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -895,8 +886,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -916,6 +906,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -926,33 +918,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2340,21 +2335,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2366,6 +2385,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2374,76 +2396,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v02-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-07-15 08:37 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index bf87430cf01..4db8a16f691 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -603,11 +602,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -621,11 +617,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -699,6 +690,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -875,11 +868,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -895,8 +886,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -916,6 +906,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -926,33 +918,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2340,21 +2335,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2366,6 +2385,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2374,76 +2396,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v02-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-07-15 08:37 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index bf87430cf01..4db8a16f691 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -603,11 +602,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -621,11 +617,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -699,6 +690,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -875,11 +868,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -895,8 +886,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -916,6 +906,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -926,33 +918,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2340,21 +2335,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2366,6 +2385,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2374,76 +2396,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v02-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-07-15 08:37 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index bf87430cf01..4db8a16f691 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -603,11 +602,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -621,11 +617,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -699,6 +690,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -875,11 +868,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -895,8 +886,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -916,6 +906,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -926,33 +918,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2340,21 +2335,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2366,6 +2385,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2374,76 +2396,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v02-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-07-15 08:37 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index bf87430cf01..4db8a16f691 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -603,11 +602,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -621,11 +617,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -699,6 +690,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -875,11 +868,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -895,8 +886,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -916,6 +906,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -926,33 +918,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2340,21 +2335,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2366,6 +2385,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2374,76 +2396,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v02-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-07-15 08:37 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index bf87430cf01..4db8a16f691 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -603,11 +602,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -621,11 +617,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -699,6 +690,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -875,11 +868,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -895,8 +886,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -916,6 +906,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -926,33 +918,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2340,21 +2335,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2366,6 +2385,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2374,76 +2396,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v02-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-07-15 08:37 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index bf87430cf01..4db8a16f691 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -603,11 +602,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -621,11 +617,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -699,6 +690,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -875,11 +868,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -895,8 +886,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -916,6 +906,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -926,33 +918,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2340,21 +2335,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2366,6 +2385,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2374,76 +2396,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v02-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-07-15 08:37 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index bf87430cf01..4db8a16f691 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -603,11 +602,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -621,11 +617,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -699,6 +690,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -875,11 +868,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -895,8 +886,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -916,6 +906,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -926,33 +918,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2340,21 +2335,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2366,6 +2385,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2374,76 +2396,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v02-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-07-15 08:37 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index bf87430cf01..4db8a16f691 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -603,11 +602,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -621,11 +617,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -699,6 +690,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -875,11 +868,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -895,8 +886,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -916,6 +906,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -926,33 +918,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2340,21 +2335,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2366,6 +2385,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2374,76 +2396,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v02-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-07-15 08:37 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index bf87430cf01..4db8a16f691 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -603,11 +602,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -621,11 +617,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -699,6 +690,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -875,11 +868,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -895,8 +886,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -916,6 +906,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -926,33 +918,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2340,21 +2335,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2366,6 +2385,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2374,76 +2396,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v02-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-07-15 08:37 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index bf87430cf01..4db8a16f691 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -603,11 +602,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -621,11 +617,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -699,6 +690,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -875,11 +868,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -895,8 +886,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -916,6 +906,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -926,33 +918,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2340,21 +2335,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2366,6 +2385,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2374,76 +2396,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v02-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-07-15 08:37 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index bf87430cf01..4db8a16f691 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -603,11 +602,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -621,11 +617,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -699,6 +690,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -875,11 +868,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -895,8 +886,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -916,6 +906,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -926,33 +918,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2340,21 +2335,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2366,6 +2385,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2374,76 +2396,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v02-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-07-15 08:37 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index bf87430cf01..4db8a16f691 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -603,11 +602,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -621,11 +617,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -699,6 +690,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -875,11 +868,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -895,8 +886,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -916,6 +906,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -926,33 +918,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2340,21 +2335,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2366,6 +2385,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2374,76 +2396,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v02-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-07-15 08:37 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index bf87430cf01..4db8a16f691 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -603,11 +602,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -621,11 +617,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -699,6 +690,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -875,11 +868,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -895,8 +886,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -916,6 +906,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -926,33 +918,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2340,21 +2335,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2366,6 +2385,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2374,76 +2396,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v02-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-07-15 08:37 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index bf87430cf01..4db8a16f691 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -603,11 +602,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -621,11 +617,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -699,6 +690,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -875,11 +868,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -895,8 +886,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -916,6 +906,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -926,33 +918,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2340,21 +2335,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2366,6 +2385,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2374,76 +2396,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v02-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-07-15 08:37 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index bf87430cf01..4db8a16f691 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -603,11 +602,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -621,11 +617,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -699,6 +690,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -875,11 +868,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -895,8 +886,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -916,6 +906,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -926,33 +918,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2340,21 +2335,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2366,6 +2385,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2374,76 +2396,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v02-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-07-15 08:37 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index bf87430cf01..4db8a16f691 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -603,11 +602,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -621,11 +617,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -699,6 +690,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -875,11 +868,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -895,8 +886,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -916,6 +906,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -926,33 +918,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2340,21 +2335,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2366,6 +2385,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2374,76 +2396,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v02-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-07-15 08:37 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index bf87430cf01..4db8a16f691 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -603,11 +602,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -621,11 +617,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -699,6 +690,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -875,11 +868,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -895,8 +886,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -916,6 +906,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -926,33 +918,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2340,21 +2335,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2366,6 +2385,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2374,76 +2396,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v02-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-07-15 08:37 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index bf87430cf01..4db8a16f691 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -603,11 +602,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -621,11 +617,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -699,6 +690,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -875,11 +868,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -895,8 +886,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -916,6 +906,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -926,33 +918,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2340,21 +2335,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2366,6 +2385,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2374,76 +2396,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v02-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-07-15 08:37 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index bf87430cf01..4db8a16f691 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -603,11 +602,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -621,11 +617,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -699,6 +690,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -875,11 +868,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -895,8 +886,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -916,6 +906,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -926,33 +918,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2340,21 +2335,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2366,6 +2385,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2374,76 +2396,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v02-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-07-15 08:37 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index bf87430cf01..4db8a16f691 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -603,11 +602,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -621,11 +617,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -699,6 +690,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -875,11 +868,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -895,8 +886,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -916,6 +906,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -926,33 +918,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2340,21 +2335,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2366,6 +2385,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2374,76 +2396,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v02-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-07-15 08:37 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index bf87430cf01..4db8a16f691 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -603,11 +602,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -621,11 +617,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -699,6 +690,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -875,11 +868,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -895,8 +886,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -916,6 +906,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -926,33 +918,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2340,21 +2335,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2366,6 +2385,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2374,76 +2396,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v02-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-07-15 08:37 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index bf87430cf01..4db8a16f691 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -603,11 +602,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -621,11 +617,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -699,6 +690,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -875,11 +868,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -895,8 +886,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -916,6 +906,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -926,33 +918,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2340,21 +2335,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2366,6 +2385,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2374,76 +2396,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v02-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-07-15 08:37 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index bf87430cf01..4db8a16f691 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -603,11 +602,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -621,11 +617,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -699,6 +690,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -875,11 +868,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -895,8 +886,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -916,6 +906,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -926,33 +918,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2340,21 +2335,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2366,6 +2385,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2374,76 +2396,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v02-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-07-15 08:37 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index bf87430cf01..4db8a16f691 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -603,11 +602,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -621,11 +617,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -699,6 +690,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -875,11 +868,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -895,8 +886,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -916,6 +906,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -926,33 +918,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2340,21 +2335,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2366,6 +2385,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2374,76 +2396,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v02-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-07-15 08:37 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index bf87430cf01..4db8a16f691 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -603,11 +602,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -621,11 +617,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -699,6 +690,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -875,11 +868,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -895,8 +886,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -916,6 +906,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -926,33 +918,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2340,21 +2335,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2366,6 +2385,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2374,76 +2396,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v02-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-07-15 08:37 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index bf87430cf01..4db8a16f691 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -603,11 +602,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -621,11 +617,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -699,6 +690,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -875,11 +868,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -895,8 +886,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -916,6 +906,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -926,33 +918,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2340,21 +2335,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2366,6 +2385,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2374,76 +2396,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v02-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-07-15 08:37 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index bf87430cf01..4db8a16f691 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -603,11 +602,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -621,11 +617,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -699,6 +690,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -875,11 +868,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -895,8 +886,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -916,6 +906,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -926,33 +918,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2340,21 +2335,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2366,6 +2385,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2374,76 +2396,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v02-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-07-15 08:37 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index bf87430cf01..4db8a16f691 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -603,11 +602,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -621,11 +617,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -699,6 +690,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -875,11 +868,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -895,8 +886,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -916,6 +906,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -926,33 +918,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2340,21 +2335,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2366,6 +2385,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2374,76 +2396,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v02-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-07-15 08:37 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index bf87430cf01..4db8a16f691 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -603,11 +602,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -621,11 +617,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -699,6 +690,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -875,11 +868,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -895,8 +886,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -916,6 +906,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -926,33 +918,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2340,21 +2335,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2366,6 +2385,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2374,76 +2396,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v02-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-07-15 08:37 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index bf87430cf01..4db8a16f691 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -603,11 +602,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -621,11 +617,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -699,6 +690,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -875,11 +868,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -895,8 +886,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -916,6 +906,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -926,33 +918,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2340,21 +2335,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2366,6 +2385,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2374,76 +2396,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v02-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-07-15 08:37 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index bf87430cf01..4db8a16f691 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -603,11 +602,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -621,11 +617,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -699,6 +690,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -875,11 +868,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -895,8 +886,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -916,6 +906,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -926,33 +918,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2340,21 +2335,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2366,6 +2385,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2374,76 +2396,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v02-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-07-15 08:37 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index bf87430cf01..4db8a16f691 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -603,11 +602,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -621,11 +617,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -699,6 +690,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -875,11 +868,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -895,8 +886,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -916,6 +906,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -926,33 +918,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2340,21 +2335,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2366,6 +2385,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2374,76 +2396,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v02-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-07-15 08:37 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index bf87430cf01..4db8a16f691 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -603,11 +602,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -621,11 +617,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -699,6 +690,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -875,11 +868,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -895,8 +886,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -916,6 +906,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -926,33 +918,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2340,21 +2335,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2366,6 +2385,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2374,76 +2396,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v02-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-07-15 08:37 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index bf87430cf01..4db8a16f691 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -603,11 +602,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -621,11 +617,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -699,6 +690,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -875,11 +868,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -895,8 +886,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -916,6 +906,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -926,33 +918,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2340,21 +2335,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2366,6 +2385,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2374,76 +2396,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v02-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-07-15 08:37 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index bf87430cf01..4db8a16f691 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -603,11 +602,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -621,11 +617,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -699,6 +690,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -875,11 +868,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -895,8 +886,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -916,6 +906,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -926,33 +918,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2340,21 +2335,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2366,6 +2385,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2374,76 +2396,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v02-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-07-15 08:37 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index bf87430cf01..4db8a16f691 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -603,11 +602,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -621,11 +617,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -699,6 +690,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -875,11 +868,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -895,8 +886,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -916,6 +906,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -926,33 +918,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2340,21 +2335,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2366,6 +2385,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2374,76 +2396,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v02-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-07-15 08:37 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index bf87430cf01..4db8a16f691 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -603,11 +602,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -621,11 +617,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -699,6 +690,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -875,11 +868,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -895,8 +886,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -916,6 +906,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -926,33 +918,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2340,21 +2335,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2366,6 +2385,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2374,76 +2396,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v02-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-07-15 08:37 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index bf87430cf01..4db8a16f691 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -603,11 +602,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -621,11 +617,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -699,6 +690,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -875,11 +868,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -895,8 +886,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -916,6 +906,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -926,33 +918,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2340,21 +2335,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2366,6 +2385,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2374,76 +2396,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v02-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-07-15 08:37 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index bf87430cf01..4db8a16f691 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -603,11 +602,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -621,11 +617,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -699,6 +690,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -875,11 +868,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -895,8 +886,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -916,6 +906,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -926,33 +918,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2340,21 +2335,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2366,6 +2385,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2374,76 +2396,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v02-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-07-15 08:37 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index bf87430cf01..4db8a16f691 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -603,11 +602,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -621,11 +617,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -699,6 +690,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -875,11 +868,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -895,8 +886,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -916,6 +906,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -926,33 +918,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2340,21 +2335,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2366,6 +2385,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2374,76 +2396,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v02-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-07-15 08:37 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index bf87430cf01..4db8a16f691 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -603,11 +602,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -621,11 +617,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -699,6 +690,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -875,11 +868,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -895,8 +886,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -916,6 +906,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -926,33 +918,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2340,21 +2335,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2366,6 +2385,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2374,76 +2396,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v02-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-07-15 08:37 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index bf87430cf01..4db8a16f691 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -603,11 +602,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -621,11 +617,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -699,6 +690,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -875,11 +868,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -895,8 +886,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -916,6 +906,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -926,33 +918,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2340,21 +2335,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2366,6 +2385,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2374,76 +2396,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v02-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-07-15 08:37 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index bf87430cf01..4db8a16f691 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -603,11 +602,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -621,11 +617,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -699,6 +690,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -875,11 +868,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -895,8 +886,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -916,6 +906,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -926,33 +918,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2340,21 +2335,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2366,6 +2385,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2374,76 +2396,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v02-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-07-15 08:37 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index bf87430cf01..4db8a16f691 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -603,11 +602,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -621,11 +617,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -699,6 +690,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -875,11 +868,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -895,8 +886,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -916,6 +906,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -926,33 +918,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2340,21 +2335,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2366,6 +2385,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2374,76 +2396,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v02-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-07-15 08:37 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index bf87430cf01..4db8a16f691 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -603,11 +602,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -621,11 +617,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -699,6 +690,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -875,11 +868,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -895,8 +886,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -916,6 +906,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -926,33 +918,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2340,21 +2335,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2366,6 +2385,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2374,76 +2396,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v02-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-07-15 08:37 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index bf87430cf01..4db8a16f691 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -603,11 +602,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -621,11 +617,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -699,6 +690,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -875,11 +868,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -895,8 +886,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -916,6 +906,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -926,33 +918,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2340,21 +2335,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2366,6 +2385,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2374,76 +2396,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v02-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-07-15 08:37 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index bf87430cf01..4db8a16f691 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -603,11 +602,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -621,11 +617,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -699,6 +690,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -875,11 +868,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -895,8 +886,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -916,6 +906,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -926,33 +918,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2340,21 +2335,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2366,6 +2385,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2374,76 +2396,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v02-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-07-15 08:37 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index bf87430cf01..4db8a16f691 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -603,11 +602,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -621,11 +617,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -699,6 +690,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -875,11 +868,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -895,8 +886,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -916,6 +906,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -926,33 +918,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2340,21 +2335,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2366,6 +2385,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2374,76 +2396,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v02-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-07-15 08:37 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index bf87430cf01..4db8a16f691 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -603,11 +602,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -621,11 +617,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -699,6 +690,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -875,11 +868,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -895,8 +886,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -916,6 +906,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -926,33 +918,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2340,21 +2335,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2366,6 +2385,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2374,76 +2396,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v02-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-07-15 08:37 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index bf87430cf01..4db8a16f691 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -603,11 +602,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -621,11 +617,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -699,6 +690,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -875,11 +868,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -895,8 +886,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -916,6 +906,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -926,33 +918,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2340,21 +2335,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2366,6 +2385,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2374,76 +2396,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v02-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-07-15 08:37 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index bf87430cf01..4db8a16f691 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -603,11 +602,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -621,11 +617,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -699,6 +690,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -875,11 +868,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -895,8 +886,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -916,6 +906,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -926,33 +918,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2340,21 +2335,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2366,6 +2385,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2374,76 +2396,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v02-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-07-15 08:37 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index bf87430cf01..4db8a16f691 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -603,11 +602,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -621,11 +617,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -699,6 +690,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -875,11 +868,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -895,8 +886,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -916,6 +906,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -926,33 +918,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2340,21 +2335,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2366,6 +2385,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2374,76 +2396,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v02-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-07-15 08:37 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index bf87430cf01..4db8a16f691 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -603,11 +602,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -621,11 +617,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -699,6 +690,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -875,11 +868,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -895,8 +886,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -916,6 +906,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -926,33 +918,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2340,21 +2335,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2366,6 +2385,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2374,76 +2396,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v02-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-07-15 08:37 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index bf87430cf01..4db8a16f691 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -603,11 +602,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -621,11 +617,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -699,6 +690,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -875,11 +868,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -895,8 +886,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -916,6 +906,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -926,33 +918,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2340,21 +2335,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2366,6 +2385,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2374,76 +2396,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v02-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-07-15 08:37 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index bf87430cf01..4db8a16f691 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -603,11 +602,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -621,11 +617,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -699,6 +690,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -875,11 +868,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -895,8 +886,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -916,6 +906,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -926,33 +918,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2340,21 +2335,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2366,6 +2385,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2374,76 +2396,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v02-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-07-15 08:37 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index bf87430cf01..4db8a16f691 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -603,11 +602,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -621,11 +617,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -699,6 +690,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -875,11 +868,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -895,8 +886,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -916,6 +906,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -926,33 +918,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2340,21 +2335,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2366,6 +2385,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2374,76 +2396,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v02-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-07-15 08:37 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index bf87430cf01..4db8a16f691 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -603,11 +602,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -621,11 +617,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -699,6 +690,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -875,11 +868,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -895,8 +886,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -916,6 +906,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -926,33 +918,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2340,21 +2335,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2366,6 +2385,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2374,76 +2396,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v02-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-07-15 08:37 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index bf87430cf01..4db8a16f691 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -603,11 +602,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -621,11 +617,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -699,6 +690,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -875,11 +868,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -895,8 +886,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -916,6 +906,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -926,33 +918,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2340,21 +2335,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2366,6 +2385,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2374,76 +2396,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v02-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-07-15 08:37 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index bf87430cf01..4db8a16f691 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -603,11 +602,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -621,11 +617,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -699,6 +690,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -875,11 +868,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -895,8 +886,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -916,6 +906,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -926,33 +918,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2340,21 +2335,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2366,6 +2385,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2374,76 +2396,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v02-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-07-15 08:37 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index bf87430cf01..4db8a16f691 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -603,11 +602,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -621,11 +617,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -699,6 +690,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -875,11 +868,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -895,8 +886,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -916,6 +906,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -926,33 +918,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2340,21 +2335,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2366,6 +2385,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2374,76 +2396,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v02-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-07-15 08:37 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index bf87430cf01..4db8a16f691 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -603,11 +602,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -621,11 +617,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -699,6 +690,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -875,11 +868,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -895,8 +886,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -916,6 +906,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -926,33 +918,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2340,21 +2335,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2366,6 +2385,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2374,76 +2396,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v02-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-07-15 08:37 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index bf87430cf01..4db8a16f691 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -603,11 +602,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -621,11 +617,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -699,6 +690,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -875,11 +868,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -895,8 +886,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -916,6 +906,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -926,33 +918,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2340,21 +2335,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2366,6 +2385,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2374,76 +2396,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v02-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-07-15 08:37 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index bf87430cf01..4db8a16f691 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -603,11 +602,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -621,11 +617,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -699,6 +690,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -875,11 +868,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -895,8 +886,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -916,6 +906,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -926,33 +918,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2340,21 +2335,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2366,6 +2385,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2374,76 +2396,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v02-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
* [PATCH 1/8] Use tuple slot to pass tuples for rewriting. @ 2026-07-15 08:37 Antonin Houska <[email protected]> 0 siblings, 0 replies; 290+ messages in thread From: Antonin Houska @ 2026-07-15 08:37 UTC (permalink / raw) This patch tries to adopt the preferable way of handling tuples, i.e. pass the containing tuple slot rather than the actual tuple. The motivation is that heap_insert_for_repack() will need a slot in the near future, in order to call ExecInsertIndexTuples(). Also, in order to set values of dropped attributes to to NULL, it seems more compact to pass a tuple slot as a workspace than two arrays (one for values and one for nulls). --- src/backend/access/heap/heapam_handler.c | 199 +++++++++++++---------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index bf87430cf01..4db8a16f691 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -48,14 +48,13 @@ #include "utils/rel.h" #include "utils/tuplesort.h" -static void reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate); -static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull, - BulkInsertState bistate); -static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, - Relation NewHeap, Datum *values, bool *isnull); +static void reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate); +static void heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, + BulkInsertStateData *bistate); +static bool tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc); +static void clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -603,11 +602,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool is_system_catalog; Tuplesortstate *tuplesort; TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); TupleTableSlot *slot; - int natts; - Datum *values; - bool *isnull; + TupleTableSlot *reform_slot; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; bool concurrent = snapshot != NULL; @@ -621,11 +617,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber); - /* Preallocate values/isnull arrays */ - natts = newTupDesc->natts; - values = palloc_array(Datum, natts); - isnull = palloc_array(bool, natts); - /* * In non-concurrent mode, initialize the rewrite operation. This is not * needed in concurrent mode. @@ -699,6 +690,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, slot = table_slot_create(OldHeap, NULL); hslot = (BufferHeapTupleTableSlot *) slot; + reform_slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsVirtual); /* * Scan through the OldHeap, either in OldIndex order or sequentially; @@ -875,11 +868,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, int64 ct_val[2]; if (!concurrent) - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -895,8 +886,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, index_endscan(indexScan); if (tableScan != NULL) table_endscan(tableScan); - if (slot) - ExecDropSingleTupleTableSlot(slot); + ExecDropSingleTupleTableSlot(slot); /* * In scan-and-sort mode, complete the sort, then read out all live tuples @@ -916,6 +906,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, pgstat_progress_update_param(PROGRESS_REPACK_PHASE, PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); + slot = MakeSingleTupleTableSlot(RelationGetDescr(OldHeap), + &TTSOpsHeapTuple); for (;;) { HeapTuple tuple; @@ -926,33 +918,36 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, if (tuple == NULL) break; + /* + * XXX Ideally we should use tuplesort_gettupleslot() above, but + * it retrieves minimal tuples and tuplesort_puttupleslot() cannot + * get the tuple descriptor from tuplesort created by + * tuplesort_begin_cluster(). + */ + ExecStoreHeapTuple(tuple, slot, false); + n_tuples += 1; if (!concurrent) - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + reform_and_rewrite_tuple(slot, reform_slot, rwstate); else - heap_insert_for_repack(tuple, OldHeap, NewHeap, - values, isnull, bistate); + heap_insert_for_repack(NewHeap, slot, reform_slot, bistate); /* Report n_tuples */ pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } + ExecDropSingleTupleTableSlot(slot); tuplesort_end(tuplesort); } + ExecDropSingleTupleTableSlot(reform_slot); + /* Write out any remaining tuples, and fsync if needed */ if (rwstate) end_heap_rewrite(rwstate); if (bistate) FreeBulkInsertState(bistate); - - /* Clean up */ - pfree(values); - pfree(isnull); } /* @@ -2340,21 +2335,45 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, * currently only known to happen as an after-effect of ALTER TABLE * SET WITHOUT OIDS. * - * So, we must reconstruct the tuple from component Datums. + * So, we must reconstruct the tuple from component Datums. 'reform' slot + * is a workspace for this reconstruction. */ static void -reform_and_rewrite_tuple(HeapTuple tuple, - Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, RewriteState rwstate) +reform_and_rewrite_tuple(TupleTableSlot *src, TupleTableSlot *reform, + RewriteState rwstate) { - HeapTuple newtuple; + HeapTuple tuple, + newtuple; + bool shouldFree, + shouldFreeNew; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + /* + * The old tuple will not be modified, so do not request materialization. + * (A copy can be created for specific slot type though, e.g. + * TTSOpsMinimalTuple.) + */ + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + + /* No need to materialize, copy will be created anyway. */ + Assert(TTS_IS_VIRTUAL(reform)); + newtuple = ExecFetchSlotHeapTuple(reform, false, &shouldFreeNew); + } + else + { + newtuple = heap_copytuple(tuple); + shouldFreeNew = true; + } /* The heap rewrite module does the rest */ rewrite_heap_tuple(rwstate, tuple, newtuple); - heap_freetuple(newtuple); + if (shouldFree) + heap_freetuple(tuple); + if (shouldFreeNew) + heap_freetuple(newtuple); } /* @@ -2366,6 +2385,9 @@ reform_and_rewrite_tuple(HeapTuple tuple, * information). Thus we must use heap_insert() both during the * catch-up and here. * + * 'reform' is a slot to use for tuple "reforming", typically to get set + * values of dropped columns to NULL. + * * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops * this relation, so no logical replication subscription should need the data. @@ -2374,76 +2396,75 @@ reform_and_rewrite_tuple(HeapTuple tuple, * case. */ static void -heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull, BulkInsertState bistate) +heap_insert_for_repack(Relation rel, TupleTableSlot *src, + TupleTableSlot *reform, BulkInsertStateData *bistate) { - HeapTuple newtuple; + HeapTuple tuple; + bool shouldFree; + TupleTableSlot *slot; - newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + tuple = ExecFetchSlotHeapTuple(src, false, &shouldFree); + if (tuple_needs_reform(tuple, src->tts_tupleDescriptor)) + { + clear_dropped_attributes(tuple, reform); + slot = reform; + } + else + slot = src; - heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), - HEAP_INSERT_NO_LOGICAL, bistate); + /* + * clear_dropped_attributes() should have deformed the tuple, so nothing + * should depend on it now. + */ + if (shouldFree) + heap_freetuple(tuple); - heap_freetuple(newtuple); + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, bistate); } -/* - * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. - * - * Deform the given tuple, set values of dropped columns to NULL, and fill in - * any values from attmissingval; then form a new tuple and return it. If no - * attributes need to be changed, a copy of the original tuple is returned. - * Caller is responsible for freeing the returned tuple. - * - * XXX this coding assumes that both relations have the same tupledesc. - */ -static HeapTuple -reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, - Datum *values, bool *isnull) +static bool +tuple_needs_reform(HeapTuple tuple, TupleDesc tupDesc) { - TupleDesc oldTupDesc = RelationGetDescr(OldHeap); - TupleDesc newTupDesc = RelationGetDescr(NewHeap); - bool needs_reform = false; - /* * A short tuple might require values from attmissing val, so activate the * coding unconditionally in that case. The value might legitimally be * NULL otherwise, so this is slightly wasteful, but it probably beats * having to test each attribute for presence of attmissingval each time. */ - if (HeapTupleHeaderGetNatts(tuple->t_data) < newTupDesc->natts) - needs_reform = true; + if (HeapTupleHeaderGetNatts(tuple->t_data) < tupDesc->natts) + return true; - /* - * If the column has been dropped but a value is still present, we can - * optimize storage now by getting rid of it. - */ - if (!needs_reform) + /* Does it have dropped attributes? */ + for (int i = 0; i < tupDesc->natts; i++) { - for (int i = 0; i < newTupDesc->natts; i++) - { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && - !heap_attisnull(tuple, i + 1, newTupDesc)) - { - needs_reform = true; - break; - } - } + if (TupleDescCompactAttr(tupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, tupDesc)) + return true; } - /* Skip work if no changes are needed */ - if (!needs_reform) - return heap_copytuple(tuple); + return false; +} - heap_deform_tuple(tuple, oldTupDesc, values, isnull); +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Set values of dropped columns to NULL, + */ +static void +clear_dropped_attributes(HeapTuple tuple, TupleTableSlot *reform) +{ + TupleDesc tupDesc = reform->tts_tupleDescriptor; + + /* Assuming 'reform' is virtual, this deforms the tuple. */ + Assert(TTS_IS_VIRTUAL(reform)); + ExecForceStoreHeapTuple(tuple, reform, false); - for (int i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < tupDesc->natts; i++) { - if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) - isnull[i] = true; + if (TupleDescCompactAttr(tupDesc, i)->attisdropped) + reform->tts_isnull[i] = true; } - - return heap_form_tuple(newTupDesc, values, isnull); } /* -- 2.52.0 --=-=-= Content-Type: text/x-diff Content-Disposition: attachment; filename=v02-0002-Move-functions-to-repack.c.patch ^ permalink raw reply [nested|flat] 290+ messages in thread
end of thread, other threads:[~2026-07-15 08:37 UTC | newest] Thread overview: 290+ messages (download: mbox mbox.gz follow: Atom feed) -- links below jump to the message on this page -- 2026-04-01 17:54 [PATCH v49 7/7] WIP add max_repack_replication_slots Álvaro Herrera <[email protected]> 2026-06-16 11:54 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-06-16 11:54 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-06-16 11:54 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-06-16 11:54 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-06-16 11:54 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-06-16 11:54 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-06-16 11:54 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-06-16 11:54 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-06-16 11:54 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-06-16 11:54 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-06-16 11:54 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-06-16 11:54 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-06-16 11:54 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-06-16 11:54 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-06-16 11:54 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-06-16 11:54 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-06-16 11:54 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-06-16 11:54 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-06-16 11:54 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-06-16 11:54 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-06-16 11:54 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-06-16 11:54 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-06-16 11:54 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-06-16 11:54 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-06-16 11:54 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-06-16 11:54 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-06-16 11:54 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-06-16 11:54 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-06-16 11:54 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-06-16 11:54 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-06-16 11:54 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-06-16 11:54 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-06-16 11:54 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-06-16 11:54 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-06-16 11:54 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-06-16 11:54 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-06-16 11:54 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-06-16 11:54 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-06-16 11:54 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-06-16 11:54 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-06-16 11:54 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-06-16 11:54 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-06-16 11:54 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-06-16 11:54 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-06-16 11:54 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-06-16 11:54 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-06-16 11:54 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-06-16 11:54 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-06-16 11:54 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-06-16 11:54 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-06-16 11:54 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-06-16 11:54 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-06-16 11:54 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-06-16 11:54 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-06-16 11:54 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-06-16 11:54 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-06-16 11:54 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-06-16 11:54 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-06-16 11:54 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-06-16 11:54 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-06-16 11:54 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-06-16 11:54 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-06-16 11:54 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-06-16 11:54 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-06-16 11:54 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-06-16 11:54 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-06-16 11:54 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-06-16 11:54 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-06-16 11:54 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-06-16 11:54 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-06-16 11:54 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-06-16 11:54 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-06-16 11:54 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-06-16 11:54 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-06-16 11:54 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-06-16 11:54 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-06-16 11:54 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-06-16 11:54 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-06-16 11:54 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-06-16 11:54 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-06-16 11:54 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-06-16 11:54 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-06-16 11:54 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-06-16 11:54 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-06-16 11:54 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-06-16 11:54 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-06-16 11:54 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-06-16 11:54 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-06-16 11:54 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-06-16 11:54 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-06-16 11:54 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-06-16 11:54 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-06-16 11:54 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-06-16 11:54 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-06-16 11:54 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-06-16 11:54 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-06-16 11:54 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-06-16 11:54 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-06-16 11:54 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-06-16 11:54 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-06-16 11:54 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-06-16 11:54 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-06-16 11:54 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-06-16 11:54 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-06-16 11:54 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-06-16 11:54 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-06-16 11:54 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-06-16 11:54 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-06-16 11:54 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-06-16 11:54 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-06-16 11:54 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-06-16 11:54 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-06-16 11:54 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-06-16 11:54 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-06-16 11:54 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-06-16 11:54 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-06-16 11:54 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-06-16 11:54 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-06-16 11:54 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-06-16 11:54 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-06-16 11:54 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-06-16 11:54 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-06-16 11:54 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-06-16 11:54 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-06-16 11:54 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-06-16 11:54 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-06-16 11:54 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-06-16 11:54 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-06-16 11:54 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-06-16 11:54 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-06-16 11:54 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-06-16 11:54 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-06-16 11:54 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-06-16 11:54 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-06-16 11:54 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-06-16 11:54 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-06-16 11:54 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-06-16 11:54 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-06-16 11:54 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-06-16 11:54 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-06-16 11:54 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-06-16 11:54 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-06-16 11:54 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-06-16 11:54 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-06-16 11:54 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-07-15 08:37 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-07-15 08:37 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-07-15 08:37 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-07-15 08:37 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-07-15 08:37 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-07-15 08:37 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-07-15 08:37 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-07-15 08:37 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-07-15 08:37 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-07-15 08:37 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-07-15 08:37 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-07-15 08:37 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-07-15 08:37 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-07-15 08:37 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-07-15 08:37 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-07-15 08:37 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-07-15 08:37 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-07-15 08:37 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-07-15 08:37 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-07-15 08:37 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-07-15 08:37 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-07-15 08:37 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-07-15 08:37 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-07-15 08:37 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-07-15 08:37 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-07-15 08:37 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-07-15 08:37 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-07-15 08:37 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-07-15 08:37 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-07-15 08:37 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-07-15 08:37 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-07-15 08:37 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-07-15 08:37 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-07-15 08:37 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-07-15 08:37 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-07-15 08:37 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-07-15 08:37 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-07-15 08:37 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-07-15 08:37 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-07-15 08:37 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-07-15 08:37 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-07-15 08:37 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-07-15 08:37 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-07-15 08:37 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-07-15 08:37 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-07-15 08:37 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-07-15 08:37 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-07-15 08:37 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-07-15 08:37 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-07-15 08:37 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-07-15 08:37 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-07-15 08:37 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-07-15 08:37 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-07-15 08:37 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-07-15 08:37 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-07-15 08:37 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-07-15 08:37 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-07-15 08:37 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-07-15 08:37 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-07-15 08:37 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-07-15 08:37 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-07-15 08:37 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-07-15 08:37 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-07-15 08:37 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-07-15 08:37 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-07-15 08:37 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-07-15 08:37 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-07-15 08:37 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-07-15 08:37 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-07-15 08:37 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-07-15 08:37 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-07-15 08:37 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-07-15 08:37 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-07-15 08:37 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-07-15 08:37 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-07-15 08:37 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-07-15 08:37 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-07-15 08:37 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-07-15 08:37 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-07-15 08:37 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-07-15 08:37 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-07-15 08:37 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-07-15 08:37 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-07-15 08:37 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-07-15 08:37 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-07-15 08:37 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-07-15 08:37 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-07-15 08:37 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-07-15 08:37 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-07-15 08:37 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-07-15 08:37 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-07-15 08:37 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-07-15 08:37 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-07-15 08:37 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-07-15 08:37 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-07-15 08:37 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-07-15 08:37 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-07-15 08:37 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-07-15 08:37 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-07-15 08:37 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-07-15 08:37 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-07-15 08:37 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-07-15 08:37 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-07-15 08:37 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-07-15 08:37 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-07-15 08:37 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-07-15 08:37 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-07-15 08:37 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-07-15 08:37 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-07-15 08:37 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-07-15 08:37 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-07-15 08:37 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-07-15 08:37 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-07-15 08:37 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-07-15 08:37 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-07-15 08:37 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-07-15 08:37 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-07-15 08:37 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-07-15 08:37 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-07-15 08:37 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-07-15 08:37 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-07-15 08:37 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-07-15 08:37 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-07-15 08:37 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-07-15 08:37 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-07-15 08:37 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-07-15 08:37 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-07-15 08:37 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-07-15 08:37 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-07-15 08:37 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-07-15 08:37 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-07-15 08:37 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-07-15 08:37 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-07-15 08:37 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-07-15 08:37 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-07-15 08:37 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-07-15 08:37 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-07-15 08:37 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-07-15 08:37 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-07-15 08:37 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-07-15 08:37 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-07-15 08:37 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-07-15 08:37 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]> 2026-07-15 08:37 [PATCH 1/8] Use tuple slot to pass tuples for rewriting. Antonin Houska <[email protected]>
This inbox is served by agora; see mirroring instructions for how to clone and mirror all data and code used for this inbox